zulip/web/src/sentry.ts
Anders Kaseorg f7eecb0e03 sentry: Untangle from page_params.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-06-22 18:53:56 -07:00

109 lines
3.5 KiB
TypeScript

import * as Sentry from "@sentry/browser";
import {z} from "zod";
type UserInfo = {
id?: string;
realm: string;
role?: string;
};
const sentry_params_schema = z.object({
dsn: z.string(),
environment: z.string(),
realm_key: z.string(),
sample_rate: z.number(),
server_version: z.string(),
trace_rate: z.number(),
user: z.object({id: z.number(), role: z.string()}).optional(),
});
const sentry_params_json =
window.document?.querySelector("script#sentry-params")?.textContent ?? undefined;
const sentry_params =
sentry_params_json === undefined
? undefined
: sentry_params_schema.parse(JSON.parse(sentry_params_json));
export function normalize_path(path: string, is_portico = false): string {
if (path === undefined) {
return "unknown";
}
path = path
.replace(/\/\d+(\/|$)/, "/*$1")
.replace(
/^\/(join|reactivate|new|accounts\/do_confirm|accounts\/confirm_new_email)\/[^/]+(\/?)$/,
"$1/*$2",
);
if (is_portico) {
return "portico: " + path;
}
return path;
}
export function shouldCreateSpanForRequest(url: string): boolean {
const parsed = new URL(url, window.location.href);
return parsed.pathname !== "/json/events";
}
if (sentry_params !== undefined) {
const sample_rates = new Map([
// This is controlled by shouldCreateSpanForRequest, above, but also put here for consistency
["call GET /json/events", 0],
// These requests are high-volume and do not add much data
["call POST /json/users/me/presence", 0.01],
["call POST /json/typing", 0.05],
]);
Sentry.init({
dsn: sentry_params.dsn,
environment: sentry_params.environment,
tunnel: "/error_tracing",
release: "zulip-server@" + ZULIP_VERSION,
integrations: [
new Sentry.BrowserTracing({
startTransactionOnLocationChange: false,
beforeNavigate(context) {
return {
...context,
metadata: {source: "custom"},
name: normalize_path(
window.location.pathname,
sentry_params.realm_key === "www",
),
};
},
shouldCreateSpanForRequest,
}),
],
sampleRate: sentry_params.sample_rate,
tracesSampler(samplingContext) {
const base_rate = sentry_params.trace_rate;
const name = samplingContext.transactionContext.name;
return base_rate * (sample_rates.get(name) ?? 1);
},
initialScope(scope) {
const user_role = sentry_params.user?.role ?? "Logged out";
const user_info: UserInfo = {
realm: sentry_params.realm_key,
role: user_role,
};
if (sentry_params.user !== undefined) {
user_info.id = sentry_params.user.id.toString();
}
scope.setTags({
realm: sentry_params.realm_key,
server_version: sentry_params.server_version,
user_role,
});
scope.setUser(user_info);
return scope;
},
});
} else {
// Always add the tracing extensions, so Sentry doesn't throw runtime errors if one calls
// startTransaction without having created the Sentry.BrowserTracing object.
Sentry.addTracingExtensions();
Sentry.init({});
}