@volter/twin-sentry 0.1.37 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -140
- package/api/spec/SOURCE.md +32 -0
- package/api/spec/doc-examples.json +12 -0
- package/api/spec/openapi.json.gz +0 -0
- package/api/spec/patches.json +5350 -0
- package/api/spec/sources.json +122 -0
- package/api/src/fetch.ts +18 -0
- package/api/src/generated/surface.gen.json +1 -0
- package/api/src/generated/ui.gen.json +1 -0
- package/api/src/index.ts +3 -0
- package/api/src/manifest.ts +137 -0
- package/api/src/semantics/api.ts +247 -0
- package/api/src/semantics/around.ts +36 -0
- package/api/src/semantics/doors.ts +76 -0
- package/api/src/semantics/index.ts +19 -0
- package/api/src/semantics/shared.ts +31 -0
- package/api/src/semantics/states.ts +17 -0
- package/dist/api/spec/SOURCE.md +32 -0
- package/dist/api/spec/doc-examples.json +12 -0
- package/dist/api/spec/openapi.json.gz +0 -0
- package/dist/api/spec/patches.json +5350 -0
- package/dist/api/spec/sources.json +122 -0
- package/dist/api/src/fetch.d.ts +2 -0
- package/dist/api/src/fetch.js +17 -0
- package/dist/api/src/fetch.ts +18 -0
- package/dist/api/src/generated/surface.gen.json +1 -0
- package/dist/api/src/generated/ui.gen.json +1 -0
- package/dist/api/src/index.d.ts +2 -0
- package/dist/api/src/index.js +3 -0
- package/dist/api/src/index.ts +3 -0
- package/dist/api/src/manifest.d.ts +2 -0
- package/dist/api/src/manifest.js +136 -0
- package/dist/api/src/manifest.ts +137 -0
- package/dist/api/src/semantics/api.d.ts +13 -0
- package/dist/api/src/semantics/api.js +269 -0
- package/dist/api/src/semantics/api.ts +247 -0
- package/dist/api/src/semantics/around.d.ts +2 -0
- package/dist/api/src/semantics/around.js +41 -0
- package/dist/api/src/semantics/around.ts +36 -0
- package/dist/api/src/semantics/doors.d.ts +5 -0
- package/dist/api/src/semantics/doors.js +71 -0
- package/dist/api/src/semantics/doors.ts +76 -0
- package/dist/api/src/semantics/index.d.ts +2 -0
- package/dist/api/src/semantics/index.js +15 -0
- package/dist/api/src/semantics/index.ts +19 -0
- package/dist/api/src/semantics/shared.d.ts +14 -0
- package/dist/api/src/semantics/shared.js +26 -0
- package/dist/api/src/semantics/shared.ts +31 -0
- package/dist/api/src/semantics/states.d.ts +24 -0
- package/dist/api/src/semantics/states.js +17 -0
- package/dist/api/src/semantics/states.ts +17 -0
- package/dist/ingest/spec/SOURCE.md +30 -0
- package/dist/ingest/spec/authentication.mdx.gz +0 -0
- package/dist/ingest/spec/compression.mdx.gz +0 -0
- package/dist/ingest/spec/doc-examples.json +216 -0
- package/dist/ingest/spec/envelope-items.mdx.gz +0 -0
- package/dist/ingest/spec/envelopes.mdx.gz +0 -0
- package/dist/ingest/spec/openapi.json +178 -0
- package/dist/ingest/spec/rate-limiting.mdx.gz +0 -0
- package/dist/ingest/spec/sources.json +25 -0
- package/dist/ingest/spec/transport.mdx.gz +0 -0
- package/dist/ingest/spec/write-spec.ts +192 -0
- package/dist/ingest/src/fetch.d.ts +2 -0
- package/dist/ingest/src/fetch.js +15 -0
- package/dist/ingest/src/fetch.ts +16 -0
- package/dist/ingest/src/generated/surface.gen.json +1 -0
- package/dist/ingest/src/generated/ui.gen.json +1 -0
- package/dist/ingest/src/index.d.ts +2 -0
- package/dist/ingest/src/index.js +3 -0
- package/dist/ingest/src/index.ts +3 -0
- package/dist/ingest/src/manifest.d.ts +2 -0
- package/dist/ingest/src/manifest.js +37 -0
- package/dist/ingest/src/manifest.ts +42 -0
- package/dist/ingest/src/semantics/api.d.ts +2 -0
- package/dist/ingest/src/semantics/api.js +240 -0
- package/dist/ingest/src/semantics/api.ts +188 -0
- package/dist/ingest/src/semantics/around.d.ts +2 -0
- package/dist/ingest/src/semantics/around.js +6 -0
- package/dist/ingest/src/semantics/around.ts +7 -0
- package/dist/ingest/src/semantics/index.d.ts +2 -0
- package/dist/ingest/src/semantics/index.js +4 -0
- package/dist/ingest/src/semantics/index.ts +8 -0
- package/dist/ingest/src/semantics/shared.d.ts +4 -0
- package/dist/ingest/src/semantics/shared.js +2 -0
- package/dist/ingest/src/semantics/shared.ts +7 -0
- package/dist/ingest/src/semantics/states.d.ts +1 -0
- package/dist/ingest/src/semantics/states.js +2 -0
- package/dist/ingest/src/semantics/states.ts +2 -0
- package/dist/journeys/coverage.json +205 -0
- package/dist/journeys/customer-life.json +921 -0
- package/dist/journeys/customer-life.outline.md +51 -0
- package/dist/journeys/decisions.json +170 -0
- package/dist/journeys/demand.json +92 -0
- package/dist/journeys/first-use.json +22 -0
- package/dist/journeys/score.json +231 -0
- package/dist/journeys/unreachable.json +29 -0
- package/dist/journeys/vendor-examples.json +474 -0
- package/dist/spec/sources.json +76 -0
- package/dist/src/cli.js +12 -27
- package/dist/src/fetch.d.ts +2 -0
- package/dist/src/fetch.js +9 -0
- package/dist/src/index.d.ts +5 -15
- package/dist/src/index.js +13 -75
- package/dist/src/manifest.d.ts +2 -0
- package/dist/src/manifest.js +35 -0
- package/dist/src/semantics/shared.d.ts +88 -0
- package/dist/src/semantics/shared.js +407 -0
- package/dist/src/server.d.ts +7 -0
- package/dist/src/server.js +7 -0
- package/generated/pack-facts.json +101 -0
- package/ingest/spec/SOURCE.md +30 -0
- package/ingest/spec/authentication.mdx.gz +0 -0
- package/ingest/spec/compression.mdx.gz +0 -0
- package/ingest/spec/doc-examples.json +216 -0
- package/ingest/spec/envelope-items.mdx.gz +0 -0
- package/ingest/spec/envelopes.mdx.gz +0 -0
- package/ingest/spec/openapi.json +178 -0
- package/ingest/spec/rate-limiting.mdx.gz +0 -0
- package/ingest/spec/sources.json +25 -0
- package/ingest/spec/transport.mdx.gz +0 -0
- package/ingest/spec/write-spec.ts +192 -0
- package/ingest/src/fetch.ts +16 -0
- package/ingest/src/generated/surface.gen.json +1 -0
- package/ingest/src/generated/ui.gen.json +1 -0
- package/ingest/src/index.ts +3 -0
- package/ingest/src/manifest.ts +42 -0
- package/ingest/src/semantics/api.ts +188 -0
- package/ingest/src/semantics/around.ts +7 -0
- package/ingest/src/semantics/index.ts +8 -0
- package/ingest/src/semantics/shared.ts +7 -0
- package/ingest/src/semantics/states.ts +2 -0
- package/journeys/coverage.json +205 -0
- package/journeys/customer-life.json +921 -0
- package/journeys/customer-life.outline.md +51 -0
- package/journeys/decisions.json +170 -0
- package/journeys/demand.json +92 -0
- package/journeys/first-use.json +22 -0
- package/journeys/score.json +231 -0
- package/journeys/unreachable.json +29 -0
- package/journeys/vendor-examples.json +474 -0
- package/package.json +22 -47
- package/spec/sources.json +76 -0
- package/src/cli.ts +12 -24
- package/src/fetch.ts +10 -0
- package/src/index.ts +13 -143
- package/src/manifest.ts +41 -0
- package/src/semantics/shared.ts +439 -0
- package/src/server.ts +8 -0
- package/client/sentry-mirror.css +0 -99
- package/client/sentry-mirror.tsx +0 -352
- package/dist/client/sentry-mirror.bundle.js +0 -388
- package/dist/client/sentry-mirror.css +0 -99
- package/dist/client/sentry-mirror.d.ts +0 -17
- package/dist/client/sentry-mirror.js +0 -156
- package/dist/client/sentry-mirror.tsx +0 -352
- package/dist/src/sentry-budget.d.ts +0 -50
- package/dist/src/sentry-budget.js +0 -145
- package/dist/src/sentry-capabilities.d.ts +0 -3
- package/dist/src/sentry-capabilities.js +0 -1180
- package/dist/src/sentry-conformance.d.ts +0 -22
- package/dist/src/sentry-conformance.js +0 -97
- package/dist/src/sentry-connector.d.ts +0 -77
- package/dist/src/sentry-connector.js +0 -226
- package/dist/src/sentry-events.d.ts +0 -54
- package/dist/src/sentry-events.js +0 -132
- package/dist/src/sentry-ingest.d.ts +0 -137
- package/dist/src/sentry-ingest.js +0 -387
- package/dist/src/sentry-mirror-ui.d.ts +0 -38
- package/dist/src/sentry-mirror-ui.js +0 -162
- package/dist/src/sentry-perform-harness.d.ts +0 -9
- package/dist/src/sentry-perform-harness.js +0 -20
- package/dist/src/sentry-server.d.ts +0 -14
- package/dist/src/sentry-server.js +0 -27
- package/dist/src/sentry-twin.d.ts +0 -17
- package/dist/src/sentry-twin.js +0 -1739
- package/dist/test-fixtures/sentry-openapi-operations.SOURCE.md +0 -24
- package/dist/test-fixtures/sentry-openapi-operations.json +0 -1837
- package/src/sentry-budget.ts +0 -171
- package/src/sentry-capabilities.ts +0 -1222
- package/src/sentry-conformance.ts +0 -110
- package/src/sentry-connector.ts +0 -260
- package/src/sentry-events.ts +0 -172
- package/src/sentry-ingest.ts +0 -471
- package/src/sentry-mirror-ui.ts +0 -162
- package/src/sentry-perform-harness.ts +0 -19
- package/src/sentry-server.ts +0 -35
- package/src/sentry-twin.ts +0 -1615
- package/test-fixtures/sentry-openapi-operations.SOURCE.md +0 -24
- package/test-fixtures/sentry-openapi-operations.json +0 -1837
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
// What every lane of Sentry shares (the API, ingest): its accounts, and what it keeps of what the SDKs send.
|
|
2
|
+
//
|
|
3
|
+
// Sentry's accounts: an organization, its projects, each project's client key
|
|
4
|
+
// (the DSN an SDK sends with), and the organization's auth tokens (what sentry-cli and an integration call the API
|
|
5
|
+
// with). Bootstrap makes the first project and credential; additional projects use createOrganizationProject.
|
|
6
|
+
//
|
|
7
|
+
// A DSN is `https://<public key>@o<organization id>.ingest.us.sentry.io/<project id>`
|
|
8
|
+
// (https://docs.sentry.io/concepts/key-terms/dsn-explainer/). An organization auth token is `sntrys_`, the base64 of what
|
|
9
|
+
// it is for, `_` and its secret (https://docs.sentry.io/account/auth-tokens/#organization-tokens); the World keeps a
|
|
10
|
+
// token by its SHA-256 and draws its secret from a secret it holds (ctx.secret), so no one computes it.
|
|
11
|
+
//
|
|
12
|
+
// What Sentry keeps of what the SDKs send (the ingest lane writes it, the API lane reads it): events (errors, messages,
|
|
13
|
+
// transactions, feedback) with the issues errors are grouped into, and the other kinds of envelope item (sessions,
|
|
14
|
+
// client reports, attachments, replays, profiles, cron check-ins and their monitors, logs, metrics). What no operation
|
|
15
|
+
// serves is bookkeeping (a `_` type, kept by ctx.record); both lanes are one service, so they share one store.
|
|
16
|
+
//
|
|
17
|
+
// The event's fields are the event payload's (https://develop.sentry.dev/sdk/foundations/envelopes/event-payloads/) as
|
|
18
|
+
// the API answers them (the spec's listProjectEvents and getProjectEvent schemas).
|
|
19
|
+
import type { HandlerContext } from '@volter/world-core';
|
|
20
|
+
|
|
21
|
+
type Reader = Pick<HandlerContext, 'row' | 'rowsRaw' | 'own'>;
|
|
22
|
+
export type Row = Record<string, unknown>;
|
|
23
|
+
|
|
24
|
+
/** An organization, made the first time (sentry.io's sign-up names it). */
|
|
25
|
+
export async function makeOrg(ctx: HandlerContext, slug: string, name: string, owner: string): Promise<Row | Response> {
|
|
26
|
+
const made = await ctx.createUnique(ORG, { slug, name, _owner: owner, dateCreated: ctx.occurredAt, status: { id: 'active', name: 'active' }, avatar: { avatarType: 'letter_avatar', avatarUuid: null }, isEarlyAdopter: false, require2FA: false, hasAuthProvider: false, features: [...DEFAULT_FEATURES], links: { organizationUrl: `https://${slug}.sentry.io`, regionUrl: 'https://us.sentry.io' }, allowMemberInvite: true, allowMemberProjectCreation: true, allowSuperuserAccess: false }, 'organization.create');
|
|
27
|
+
return made.created ? ctx.row(ORG, String(made.row.id))! : Response.json({ slug: ['An organization with this slug already exists.'] }, { status: 409 });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A project of an organization with its default client key (the project's creation makes one). */
|
|
31
|
+
export async function makeProject(ctx: HandlerContext, org: Row, slug: string, platform: string | null): Promise<Row | Response> {
|
|
32
|
+
const made = await ctx.createUnique(PROJECT, { slug, name: slug, platform, _organization: org.id, dateCreated: ctx.occurredAt, _issueCounter: 0, isBookmarked: false, isMember: true, features: ['releases'], firstEvent: null, firstTransactionEvent: false, access: [], hasAccess: true, team: null, teams: [], environments: [], latestRelease: null, platforms: platform ? [platform] : [], hasSessions: false, hasProfiles: false, hasReplays: false, hasFlags: false, hasMinifiedStackTrace: false, hasMonitors: false, hasFeedbacks: false, hasNewFeedbacks: false, hasUserReports: false, hasInsightsHttp: false, hasInsightsDb: false, hasInsightsAssets: false, hasInsightsAppStart: false, hasInsightsScreenLoad: false, hasInsightsVitals: false, hasInsightsCaches: false, hasInsightsQueues: false, hasInsightsAgentMonitoring: false, hasInsightsMCP: false, hasLogs: false, hasTraceMetrics: false }, 'project.create');
|
|
33
|
+
if (!made.created) return Response.json({ slug: ['A project with this slug already exists.'] }, { status: 409 });
|
|
34
|
+
const project = ctx.row(PROJECT, String(made.row.id))!;
|
|
35
|
+
await makeClientKey(ctx, org, project);
|
|
36
|
+
return project;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function makeClientKey(ctx: HandlerContext, org: Row, project: Row): Promise<Row> {
|
|
40
|
+
const id = String(project.id);
|
|
41
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/models/projectkey.py "secret_key = ProjectKey.generate_api_key()"
|
|
42
|
+
const secret = ctx.crypto.digest('sha256', await ctx.secret(`sentry-project-key:${await next(ctx, 'project_key')}`), 'hex').slice(0, 32);
|
|
43
|
+
const endpoint = `https://o${String(org.id)}.ingest.us.sentry.io`;
|
|
44
|
+
const api = `${endpoint}/api/${id}`;
|
|
45
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/api/serializers/models/project_key.py "ProjectKeySerializerResponse"
|
|
46
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/projectoptions/defaults.py "sentry:default_loader_version"
|
|
47
|
+
// New projects take the current option epoch's loader defaults; the installed application SDK is independent.
|
|
48
|
+
const made = await ctx.create(CLIENT_KEY, (pub) => ({ id: pub, public: pub, secret, _project: id, name: 'Default', label: 'Default', projectId: Number(id), isActive: true, dateCreated: ctx.occurredAt, rateLimit: null,
|
|
49
|
+
dsn: { public: `https://${pub}@${endpoint.slice(8)}/${id}`, secret: `https://${pub}:${secret}@${endpoint.slice(8)}/${id}`,
|
|
50
|
+
csp: `${api}/csp-report/?sentry_key=${pub}`, security: `${api}/security/?sentry_key=${pub}`, minidump: `${api}/minidump/?sentry_key=${pub}`, nel: `${api}/nel/?sentry_key=${pub}`,
|
|
51
|
+
playstation: `${api}/playstation/?sentry_key=${pub}`, integration: `${api}/integration/`, otlp_traces: `${api}/integration/otlp/v1/traces`, otlp_logs: `${api}/integration/otlp/v1/logs`, unreal: `${api}/unreal/${pub}/`, crons: `${api}/cron/___MONITOR_SLUG___/${pub}/`, cdn: `https://js.sentry-cdn.com/${pub}.min.js` },
|
|
52
|
+
browserSdkVersion: '11.x', browserSdk: { choices: ['11.x', '10.x', '9.x', '8.x', '7.x'].map((v) => [v, v]) }, dynamicSdkLoaderOptions: { hasReplay: true, hasPerformance: true, hasDebug: false, hasFeedback: false, hasLogsAndMetrics: false },
|
|
53
|
+
}), 'project_key.create');
|
|
54
|
+
return ctx.row(CLIENT_KEY, String(made.id))!;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** An organization auth token's value: `sntrys_`, the base64 of what it is for (when it was made, the API's and the
|
|
58
|
+
* region's URLs, the organization) and `_` and its secret, drawn from the World's secret under its label. */
|
|
59
|
+
async function tokenValue(ctx: Pick<HandlerContext, 'secret'>, org: Row, label: string, iat: number): Promise<string> {
|
|
60
|
+
const claims = Buffer.from(JSON.stringify({ iat, url: 'https://sentry.io', region_url: 'https://us.sentry.io', org: org.slug })).toString('base64');
|
|
61
|
+
const secret = (await ctx.secret(label)).replace(/[^A-Za-z0-9]/g, '').slice(0, 43);
|
|
62
|
+
return `sntrys_${claims}_${secret}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** An organization auth token, made on Settings > Auth Tokens: its value shown once. Its scopes are the organization
|
|
66
|
+
* token's fixed set ("org:ci": releases and source maps, and reading the organization). */
|
|
67
|
+
export async function makeOrgToken(ctx: HandlerContext, org: Row, name: string, scopes: string[] = ['org:ci']): Promise<string> {
|
|
68
|
+
const label = `sentry-org-token:${String(org.id)}:${await next(ctx, 'token')}`;
|
|
69
|
+
const iat = Math.floor(Date.parse(ctx.occurredAt) / 1000);
|
|
70
|
+
const token = await tokenValue(ctx, org, label, iat);
|
|
71
|
+
await ctx.record(TOKEN, { organization: org.id, name, scopes, kind: 'org', dateCreated: ctx.occurredAt, iat, label, lastChars: token.slice(-4) }, sha256(ctx, token));
|
|
72
|
+
return token;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A live native auth token, reconstructed only while its stored digest still matches. */
|
|
76
|
+
export async function readToken(ctx: HandlerContext, org: Row, row: Row): Promise<string | undefined> {
|
|
77
|
+
if (row.deleted === true || row.organization !== org.id) return undefined;
|
|
78
|
+
const token = row.kind === 'personal' ? `sntryu_${ctx.crypto.digest('sha256', await ctx.secret(String(row.label)), 'hex')}` : await tokenValue(ctx, org, String(row.label), Number(row.iat));
|
|
79
|
+
return sha256(ctx, token) === row.id ? token : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A personal token (User Settings > Personal Tokens): `sntryu_` and 64 hexadecimal digits, with the scopes chosen when it
|
|
83
|
+
* is made, acting on the organizations its user belongs to (here, the one named). */
|
|
84
|
+
export async function makePersonalToken(ctx: HandlerContext, org: Row, name: string, scopes: string[]): Promise<string> {
|
|
85
|
+
const label = `sentry-personal-token:${await next(ctx, 'token')}`;
|
|
86
|
+
const hex = ctx.crypto.digest('sha256', await ctx.secret(label), 'hex');
|
|
87
|
+
const token = `sntryu_${hex}`;
|
|
88
|
+
await ctx.record(TOKEN, { organization: org.id, user: org._owner, name, scopes, kind: 'personal', dateCreated: ctx.occurredAt, label, lastChars: token.slice(-4) }, sha256(ctx, token));
|
|
89
|
+
return token;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The token a request's `Authorization: Bearer` names, if the World issued it and it is not revoked. */
|
|
93
|
+
export function bearerToken(ctx: Pick<HandlerContext, 'row' | 'crypto' | 'call'>): Row | undefined {
|
|
94
|
+
const header = ctx.call.request.headers.get('authorization') ?? '';
|
|
95
|
+
const value = /^Bearer\s+(\S+)$/i.exec(header)?.[1];
|
|
96
|
+
return value ? live(ctx.row(TOKEN, sha256(ctx, value))) : undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Whether a token's scopes grant one: the scope itself, one its `org:ci` implies, or the same resource's write or
|
|
100
|
+
* admin (a `:write` scope reads, an `:admin` one writes and reads). */
|
|
101
|
+
export function grants(token: Row, scope: string): boolean {
|
|
102
|
+
const held = ((token.scopes as string[] | undefined) ?? []).flatMap((s) => [s, ...(IMPLIED[s] ?? [])]);
|
|
103
|
+
const [resource] = scope.split(':');
|
|
104
|
+
return held.includes(scope) || held.includes(`${resource}:admin`) || (scope.endsWith(':read') && held.includes(`${resource}:write`));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** An event's time: its `timestamp` (RFC 3339 or seconds since the epoch), else when it was received. */
|
|
108
|
+
export function eventTime(payload: Row, received: string): string {
|
|
109
|
+
const t = payload.timestamp;
|
|
110
|
+
if (typeof t === 'number') return new Date(t * 1000).toISOString();
|
|
111
|
+
if (typeof t === 'string' && !Number.isNaN(Date.parse(t))) return new Date(t).toISOString();
|
|
112
|
+
return received;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The exception an error event names first (the last of `exception.values`, the one raised), if any. */
|
|
116
|
+
function exceptionOf(payload: Row): Row | undefined {
|
|
117
|
+
const values = arr(obj(payload.exception).values ?? payload.exception);
|
|
118
|
+
return values.length ? values[values.length - 1] : undefined;
|
|
119
|
+
}
|
|
120
|
+
function framesOf(exception: Row | undefined): Row[] { return arr(obj(exception?.stacktrace).frames); }
|
|
121
|
+
/** The frame an error is located at: the innermost in-app frame, else the innermost. */
|
|
122
|
+
function topFrame(frames: Row[]): Row | undefined {
|
|
123
|
+
const inApp = frames.filter((f) => f.in_app === true);
|
|
124
|
+
return (inApp.length ? inApp : frames).at(-1);
|
|
125
|
+
}
|
|
126
|
+
function messageOf(payload: Row): string {
|
|
127
|
+
const m = payload.message ?? payload.logentry;
|
|
128
|
+
if (typeof m === 'string') return m;
|
|
129
|
+
const o = obj(m);
|
|
130
|
+
return String(o.formatted ?? o.message ?? '');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** An event's title, culprit, location and metadata, as Sentry derives them for its kind. Where the documentation stops:
|
|
134
|
+
* Sentry's own derivation (its event types' `get_title` and `get_location`) is code, not a page; these follow its
|
|
135
|
+
* answers' forms: `<Type>: <value>` for an error, the message for a message event, the transaction's name for one. */
|
|
136
|
+
export function derive(payload: Row, type: string): { title: string; culprit: string; location: string | null; metadata: Row } {
|
|
137
|
+
if (type === 'transaction') {
|
|
138
|
+
const name = String(payload.transaction ?? '<unlabeled transaction>');
|
|
139
|
+
return { title: name, culprit: name, location: name, metadata: { title: name } };
|
|
140
|
+
}
|
|
141
|
+
if (type === 'feedback') {
|
|
142
|
+
const f = obj(obj(payload.contexts).feedback);
|
|
143
|
+
const message = String(f.message ?? '');
|
|
144
|
+
return { title: 'User Feedback', culprit: 'user', location: null, metadata: { title: 'User Feedback', value: message, message, contact_email: f.contact_email ?? null, name: f.name ?? null } };
|
|
145
|
+
}
|
|
146
|
+
const ex = exceptionOf(payload);
|
|
147
|
+
const frame = topFrame(framesOf(ex));
|
|
148
|
+
const location = frame ? String(frame.filename ?? frame.abs_path ?? frame.module ?? '') || null : null;
|
|
149
|
+
const culprit = String(payload.transaction ?? payload.culprit ?? (frame ? `${String(frame.function ?? '?')}(${String(frame.filename ?? frame.abs_path ?? '?')})` : ''));
|
|
150
|
+
if (ex) {
|
|
151
|
+
const exType = String(ex.type ?? 'Error');
|
|
152
|
+
const value = String(ex.value ?? '');
|
|
153
|
+
return { title: value ? `${exType}: ${value.split('\n')[0]}` : exType, culprit, location,
|
|
154
|
+
metadata: { type: exType, value, ...(frame?.filename ? { filename: frame.filename } : {}), ...(frame?.function ? { function: frame.function } : {}), in_app_frame_mix: 'mixed' } };
|
|
155
|
+
}
|
|
156
|
+
const message = messageOf(payload) || '<unlabeled event>';
|
|
157
|
+
return { title: message.split('\n')[0]!, culprit, location, metadata: { title: message.split('\n')[0]!, value: message } };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The hash an error groups by: its fingerprint when the SDK sends one of its own, else its exception's type and its
|
|
161
|
+
* in-app frames (or its type and value, with no stack trace), else its message. Where the documentation stops: Sentry
|
|
162
|
+
* groups by its grouping configuration (https://docs.sentry.io/concepts/data-management/event-grouping/), whose
|
|
163
|
+
* algorithm is not reproduced; this keeps its rule's shape: the stack trace first, then the exception, then the message. */
|
|
164
|
+
export function groupingHash(ctx: Pick<HandlerContext, 'crypto'>, payload: Row): string {
|
|
165
|
+
const fp = Array.isArray(payload.fingerprint) ? (payload.fingerprint as unknown[]).map(String) : [];
|
|
166
|
+
const own = fp.filter((f) => f !== '{{ default }}' && f !== '{{default}}');
|
|
167
|
+
const ex = exceptionOf(payload);
|
|
168
|
+
const frames = framesOf(ex).filter((f) => f.in_app !== false).map((f) => `${String(f.module ?? f.filename ?? '')}:${String(f.function ?? '')}`);
|
|
169
|
+
const basis = own.length && own.length === fp.length ? ['fp', ...own]
|
|
170
|
+
: ex ? (frames.length ? ['st', String(ex.type ?? ''), ...frames] : ['ex', String(ex.type ?? ''), String(ex.value ?? '')])
|
|
171
|
+
: ['msg', messageOf(payload)];
|
|
172
|
+
return ctx.crypto.digest('md5', JSON.stringify(basis), 'hex');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** An event's tags as the API lists them: the SDK's own, and the ones Sentry derives from the event. */
|
|
176
|
+
export function tagsOf(payload: Row): Array<{ key: string; value: string }> {
|
|
177
|
+
const out = new Map<string, string>();
|
|
178
|
+
const own = payload.tags;
|
|
179
|
+
if (Array.isArray(own)) for (const t of own) { if (Array.isArray(t)) out.set(String(t[0]), String(t[1])); }
|
|
180
|
+
else for (const [k, v] of Object.entries(obj(own))) out.set(k, String(v));
|
|
181
|
+
const put = (k: string, v: unknown): void => { if (v !== undefined && v !== null && v !== '' && !out.has(k)) out.set(k, String(v)); };
|
|
182
|
+
put('level', payload.level ?? 'error');
|
|
183
|
+
put('environment', payload.environment);
|
|
184
|
+
put('release', payload.release);
|
|
185
|
+
put('transaction', payload.transaction);
|
|
186
|
+
put('server_name', payload.server_name);
|
|
187
|
+
put('url', obj(payload.request).url);
|
|
188
|
+
const user = obj(payload.user);
|
|
189
|
+
put('user', user.email ? `email:${String(user.email)}` : user.id ? `id:${String(user.id)}` : user.ip_address ? `ip:${String(user.ip_address)}` : undefined);
|
|
190
|
+
const ex = exceptionOf(payload);
|
|
191
|
+
const mech = obj(ex?.mechanism);
|
|
192
|
+
if (ex) { put('handled', mech.handled === false ? 'no' : 'yes'); put('mechanism', mech.type); }
|
|
193
|
+
return [...out.entries()].map(([key, value]) => ({ key, value })).sort((a, b) => a.key.localeCompare(b.key));
|
|
194
|
+
}
|
|
195
|
+
/** An issue's short id's number, as Sentry writes it: `<PROJECT>-<base32 of the project's issue count>` (models/group.py). */
|
|
196
|
+
function base32(n: number): string { let out = ''; let x = n; do { out = BASE32[x % 32] + out; x = Math.floor(x / 32); } while (x > 0); return out; }
|
|
197
|
+
|
|
198
|
+
/** An error or message event kept, grouped into its issue (made, or counted again). */
|
|
199
|
+
export async function keepEvent(ctx: HandlerContext, project: Row, payload: Row, type: string): Promise<Row> {
|
|
200
|
+
const eventID = String(payload.event_id ?? '').replace(/-/g, '').toLowerCase() || ctx.mint(EVENT);
|
|
201
|
+
const id = `${String(project.id)}:${eventID}`;
|
|
202
|
+
const held = ctx.row(type === 'transaction' ? '_transaction' : EVENT, id);
|
|
203
|
+
if (held && held.deleted !== true) return held;
|
|
204
|
+
const when = eventTime(payload, ctx.occurredAt);
|
|
205
|
+
const d = derive(payload, type);
|
|
206
|
+
let groupID: string | null = null;
|
|
207
|
+
if (type !== 'transaction') {
|
|
208
|
+
const hash = type === 'feedback' ? `feedback:${eventID}` : groupingHash(ctx, payload);
|
|
209
|
+
const issue = ctx.rowsRaw(ISSUE).find((i) => i.deleted !== true && i._project === project.id && i._hash === hash);
|
|
210
|
+
const user = obj(payload.user);
|
|
211
|
+
const userKey = String(user.id ?? user.email ?? user.ip_address ?? '');
|
|
212
|
+
if (issue) {
|
|
213
|
+
// the issue's users, one subject each, which its userCount counts
|
|
214
|
+
if (userKey) await ctx.record(ISSUE_USER, { issue: issue.id, user: userKey }, `${String(issue.id)}:${userKey}`);
|
|
215
|
+
const userCount = ctx.rowsRaw(ISSUE_USER).filter((u) => u.issue === issue.id).length;
|
|
216
|
+
await ctx.write(ISSUE, String(issue.id), { count: String(Number(issue.count) + 1), userCount, lastSeen: when > String(issue.lastSeen) ? when : issue.lastSeen }, 'issue.update');
|
|
217
|
+
groupID = String(issue.id);
|
|
218
|
+
} else {
|
|
219
|
+
const n = Number(project._issueCounter ?? 0) + 1;
|
|
220
|
+
await ctx.write('project', String(project.id), { _issueCounter: n }, 'project.update');
|
|
221
|
+
groupID = ctx.mint(ISSUE);
|
|
222
|
+
await ctx.write(ISSUE, groupID, {
|
|
223
|
+
id: groupID, shortId: `${String(project.slug).toUpperCase()}-${base32(n)}`, project: { id: project.id, name: project.name, slug: project.slug, platform: project.platform ?? null }, _project: project.id, _organization: project._organization, _hash: hash,
|
|
224
|
+
title: d.title, culprit: d.culprit, level: String(payload.level ?? (type === 'feedback' ? 'info' : 'error')), status: 'unresolved', substatus: 'new',
|
|
225
|
+
type: type === 'feedback' ? 'generic' : type === 'default' ? 'default' : 'error', issueType: type === 'feedback' ? 'feedback' : 'error', issueCategory: type === 'feedback' ? 'feedback' : 'error',
|
|
226
|
+
metadata: d.metadata, count: '1', userCount: userKey ? 1 : 0, firstSeen: when, lastSeen: when, platform: payload.platform ?? project.platform ?? null,
|
|
227
|
+
}, 'issue.create');
|
|
228
|
+
const madeIssue = ctx.row(ISSUE, groupID)!;
|
|
229
|
+
await ctx.write(ISSUE, groupID, issueView(ctx, madeIssue), 'issue.update');
|
|
230
|
+
if (userKey) await ctx.record(ISSUE_USER, { issue: groupID, user: userKey }, `${groupID}:${userKey}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const fields = {
|
|
234
|
+
id: eventID, eventID, projectID: project.id, _project: project.id, _organization: project._organization, type, groupID, title: d.title, culprit: d.culprit, location: d.location, metadata: d.metadata,
|
|
235
|
+
message: messageOf(payload), platform: payload.platform ?? null, dateCreated: when, dateReceived: ctx.occurredAt, tags: tagsOf(payload),
|
|
236
|
+
user: Object.keys(obj(payload.user)).length ? obj(payload.user) : null, release: payload.release ?? null, _environment: payload.environment ?? null,
|
|
237
|
+
_payload: payload,
|
|
238
|
+
};
|
|
239
|
+
const stored = type === 'transaction' ? await ctx.record('_transaction', fields, id) : await ctx.write(EVENT, id, { ...fields, ...fullEvent(ctx, fields) }, 'event.create');
|
|
240
|
+
if (type !== 'transaction' && groupID) {
|
|
241
|
+
const siblings = ctx.rowsRaw(EVENT).filter((e) => e.groupID === groupID).sort((a, b) => String(a.dateCreated).localeCompare(String(b.dateCreated)));
|
|
242
|
+
for (let i = 0; i < siblings.length; i++) await ctx.write(EVENT, String(siblings[i]!.id), { previousEventID: siblings[i - 1]?.eventID ?? null, nextEventID: siblings[i + 1]?.eventID ?? null }, 'event.update');
|
|
243
|
+
}
|
|
244
|
+
await ctx.write(PROJECT, String(project.id), { ...(type === 'transaction' ? { firstTransactionEvent: true } : { firstEvent: project.firstEvent ?? when }), ...(type === 'feedback' ? { hasFeedbacks: true, hasNewFeedbacks: true } : {}), environments: [...new Set([...(project.environments as string[] ?? []), ...(payload.environment ? [String(payload.environment)] : [])])] }, 'project.update');
|
|
245
|
+
return typeof stored === 'string' ? { ...fields, id: stored } : stored;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** An event as a project's event list answers it (the spec's listProjectEvents item). */
|
|
249
|
+
export function simpleEvent(e: Row): Row {
|
|
250
|
+
const user = e.user ? obj(e.user) : null;
|
|
251
|
+
return {
|
|
252
|
+
id: e.eventID, 'event.type': e.type, groupID: e.groupID ?? null, eventID: e.eventID, projectID: e.projectID, message: e.message ?? '', title: e.title,
|
|
253
|
+
location: e.location ?? null, culprit: e.culprit ?? null,
|
|
254
|
+
user: user ? { id: user.id ?? null, email: user.email ?? null, username: user.username ?? null, ip_address: user.ip_address ?? null, name: user.name ?? null, geo: user.geo ?? null, data: user.data ?? null } : null,
|
|
255
|
+
tags: e.tags, platform: e.platform ?? null, dateCreated: e.dateCreated, crashFile: null, metadata: e.metadata ?? {},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** An event in full (the spec's getProjectEvent): the simple fields, its entries (the exception with its stack
|
|
260
|
+
* traces, the breadcrumbs, the request, the message), its contexts, SDK, release and neighbours. */
|
|
261
|
+
export function fullEvent(ctx: Reader, raw: Row): Row {
|
|
262
|
+
const e = raw;
|
|
263
|
+
const p = obj(raw._payload);
|
|
264
|
+
const entries: Row[] = [];
|
|
265
|
+
const ex = obj(p.exception);
|
|
266
|
+
const values = arr(ex.values ?? p.exception);
|
|
267
|
+
if (values.length) entries.push({ type: 'exception', data: { values: values.map((v) => ({ type: v.type ?? null, value: v.value ?? null, module: v.module ?? null, mechanism: v.mechanism ?? null, threadId: v.thread_id ?? null, rawStacktrace: null,
|
|
268
|
+
stacktrace: v.stacktrace ? { frames: framesOf(v).map((f) => ({ filename: f.filename ?? null, absPath: f.abs_path ?? null, module: f.module ?? null, package: f.package ?? null, platform: f.platform ?? null, instructionAddr: null, symbolAddr: null, function: f.function ?? null, rawFunction: null, symbol: null, context: [...arr(f.pre_context).map((l, i, a) => [Number(f.lineno ?? 0) - a.length + i, l]), ...(f.context_line !== undefined ? [[f.lineno ?? null, f.context_line]] : []), ...arr(f.post_context).map((l, i) => [Number(f.lineno ?? 0) + i + 1, l])], lineNo: f.lineno ?? null, colNo: f.colno ?? null, inApp: f.in_app ?? false, trust: null, errors: null, lock: null, vars: f.vars ?? null })), framesOmitted: null, registers: null, hasSystemFrames: framesOf(v).some((f) => f.in_app !== true) } : null })), hasSystemFrames: false, excOmitted: null } });
|
|
269
|
+
const crumbs = arr(obj(p.breadcrumbs).values ?? p.breadcrumbs);
|
|
270
|
+
if (crumbs.length) entries.push({ type: 'breadcrumbs', data: { values: crumbs.map((b) => ({ type: b.type ?? 'default', timestamp: typeof b.timestamp === 'number' ? new Date(b.timestamp * 1000).toISOString() : b.timestamp ?? null, level: b.level ?? 'info', message: b.message ?? null, category: b.category ?? null, data: b.data ?? null, event_id: null })) } });
|
|
271
|
+
const req = obj(p.request);
|
|
272
|
+
if (Object.keys(req).length) entries.push({ type: 'request', data: { url: req.url ?? null, method: req.method ?? null, query: req.query_string ?? [], fragment: req.fragment ?? null, data: req.data ?? null, headers: Object.entries(obj(req.headers)), cookies: [], env: req.env ?? null, inferredContentType: null, apiTarget: null } });
|
|
273
|
+
if (!values.length && e.message) entries.push({ type: 'message', data: { formatted: e.message, message: null, params: null } });
|
|
274
|
+
const siblings = ctx.rowsRaw(EVENT).filter((x) => x.deleted !== true && x.groupID === e.groupID && e.groupID !== null).sort((a, b) => String(a.dateCreated).localeCompare(String(b.dateCreated)));
|
|
275
|
+
const at = siblings.findIndex((x) => x.eventID === e.eventID);
|
|
276
|
+
return {
|
|
277
|
+
...Object.fromEntries(Object.entries(simpleEvent(e)).filter(([key]) => key !== 'event.type')), size: JSON.stringify(p).length, entries, dist: p.dist ?? null, contexts: obj(p.contexts), sdk: p.sdk ?? null, context: obj(p.extra), packages: obj(p.modules),
|
|
278
|
+
type: e.type, errors: [], occurrence: null, _meta: {}, dateReceived: e.dateReceived, fingerprints: [], groupingConfig: { id: 'newstyle:2023-01-11', enhancements: '' },
|
|
279
|
+
release: e.release ? { version: e.release } : null, userReport: null, sdkUpdates: [], resolvedWith: [],
|
|
280
|
+
nextEventID: at >= 0 && at < siblings.length - 1 ? siblings[at + 1]!.eventID : null, previousEventID: at > 0 ? siblings[at - 1]!.eventID : null,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** An issue as the API answers it (the spec's issue schema, its common fields). */
|
|
285
|
+
export function issueView(ctx: Reader, raw: Row): Row {
|
|
286
|
+
const i = ctx.own(raw);
|
|
287
|
+
const project = ctx.row('project', String(i._project));
|
|
288
|
+
const org = project ? ctx.row('organization', String(project._organization)) : undefined;
|
|
289
|
+
return {
|
|
290
|
+
id: i.id, shareId: null, shortId: i.shortId, title: i.title, culprit: i.culprit,
|
|
291
|
+
permalink: `https://${String(org?.slug ?? 'sentry')}.sentry.io/issues/${String(i.id)}/`, logger: null, level: i.level, status: i.status, substatus: i.substatus ?? null,
|
|
292
|
+
statusDetails: {}, isPublic: false, platform: i.platform ?? null, project: project ? { id: project.id, name: project.name, slug: project.slug, platform: project.platform ?? null } : null,
|
|
293
|
+
type: i.type, metadata: i.metadata, numComments: 0, assignedTo: null, isBookmarked: false, isSubscribed: false, subscriptionDetails: null, hasSeen: false, annotations: [],
|
|
294
|
+
issueType: i.issueType, issueCategory: i.issueCategory, priority: 'medium', priorityLockedAt: null, isUnhandled: false,
|
|
295
|
+
count: String(i.count), userCount: i.userCount, firstSeen: i.firstSeen, lastSeen: i.lastSeen,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Stored vendor fields, including the API's own _meta; private association/payload fields stay in the tree. */
|
|
300
|
+
export function wire(ctx: Reader, row: Row): Row {
|
|
301
|
+
return Object.fromEntries(Object.entries({ id: row.id, ...ctx.own(row) }).filter(([key]) => !key.startsWith('_') || key === '_meta'));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export const ORG = 'organization';
|
|
305
|
+
export const PROJECT = 'project';
|
|
306
|
+
export const CLIENT_KEY = 'project_key';
|
|
307
|
+
export const TOKEN = '_auth_token';
|
|
308
|
+
|
|
309
|
+
const live = (r: Row | undefined): Row | undefined => (r && r.deleted !== true ? r : undefined);
|
|
310
|
+
|
|
311
|
+
/** An id in Sentry's form: a number, as a string. Where the documentation stops: how Sentry mints its ids is not
|
|
312
|
+
* published; ctx.mint makes hash-derived decimal ids at the declared width, the same every run. */
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
export const orgBySlug = (ctx: Reader, ref: string): Row | undefined =>
|
|
316
|
+
ctx.rowsRaw(ORG).find((o) => o.deleted !== true && (o.slug === ref || o.id === ref));
|
|
317
|
+
export const projectOf = (ctx: Reader, org: Row, ref: string): Row | undefined =>
|
|
318
|
+
ctx.rowsRaw(PROJECT).find((p) => p.deleted !== true && p._organization === org.id && (p.slug === ref || p.id === ref));
|
|
319
|
+
export const projectById = (ctx: Reader, id: string): Row | undefined => live(ctx.row(PROJECT, id));
|
|
320
|
+
|
|
321
|
+
/** The host a DSN names for an organization. */
|
|
322
|
+
export const ingestHost = (org: Row): string => `o${String(org.id)}.ingest.us.sentry.io`;
|
|
323
|
+
/** A project's DSN, from its client key. */
|
|
324
|
+
export const dsnOf = (org: Row, project: Row, key: Row): string => `https://${String(key.public)}@${ingestHost(org)}/${String(project.id)}`;
|
|
325
|
+
|
|
326
|
+
/** A project's active client key. */
|
|
327
|
+
export const keyOf = (ctx: Reader, project: Row): Row | undefined =>
|
|
328
|
+
ctx.rowsRaw(CLIENT_KEY).find((k) => k.deleted !== true && k._project === project.id && k.isActive !== false);
|
|
329
|
+
/** The project a DSN's public key belongs to. */
|
|
330
|
+
export const projectByKey = (ctx: Reader, publicKey: string): Row | undefined => {
|
|
331
|
+
const key = ctx.rowsRaw(CLIENT_KEY).find((k) => k.deleted !== true && k.public === publicKey && k.isActive !== false);
|
|
332
|
+
return key ? projectById(ctx, String(key._project)) : undefined;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const sha256 = (ctx: Pick<HandlerContext, 'crypto'>, v: string): string => ctx.crypto.digest('sha256', v, 'hex');
|
|
336
|
+
|
|
337
|
+
// "Organization Tokens: ... have a limited set of permissions and are designed to be used in CI environments and with
|
|
338
|
+
// Sentry CLI" (docs.sentry.io/account/auth-tokens). Where the documentation stops: the set is Sentry's code's; the
|
|
339
|
+
// twin's `org:ci` reads the organization and makes releases and uploads source maps, which is what sentry-cli does.
|
|
340
|
+
const IMPLIED: Record<string, string[]> = { 'org:ci': ['org:read', 'project:releases'] };
|
|
341
|
+
|
|
342
|
+
/** The World's next number in a sequence (a project's id, a nonce): issued from the tree over the lane's declared
|
|
343
|
+
* `_seq_<what>` (ctx.issue), never a row count. */
|
|
344
|
+
export const next = async (ctx: Pick<HandlerContext, 'issue'>, what: string): Promise<number> => Number(await ctx.issue(`_seq_${what}`));
|
|
345
|
+
|
|
346
|
+
// ── what Sentry keeps of what the SDKs send ─────────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
export const EVENT = 'event';
|
|
349
|
+
export const ISSUE = 'issue';
|
|
350
|
+
/** One user an issue's events came from (its `userCount` counts them): bookkeeping the vendor serves only as the count. */
|
|
351
|
+
export const ISSUE_USER = '_issue_user';
|
|
352
|
+
export const SESSION = '_session';
|
|
353
|
+
export const CLIENT_REPORT = '_client_report';
|
|
354
|
+
export const ATTACHMENT = '_attachment';
|
|
355
|
+
export const REPLAY = '_replay';
|
|
356
|
+
export const PROFILE = '_profile';
|
|
357
|
+
export const MONITOR = 'monitor';
|
|
358
|
+
export const CHECK_IN = 'check_in';
|
|
359
|
+
export const LOG = '_log';
|
|
360
|
+
export const METRIC = '_metric';
|
|
361
|
+
export const OTHER_ITEM = '_envelope_item';
|
|
362
|
+
|
|
363
|
+
const obj = (v: unknown): Row => (v && typeof v === 'object' && !Array.isArray(v) ? v as Row : {});
|
|
364
|
+
const arr = (v: unknown): Row[] => (Array.isArray(v) ? v as Row[] : []);
|
|
365
|
+
|
|
366
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/utils/numbers.py "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
367
|
+
const BASE32 = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
368
|
+
|
|
369
|
+
// API-exposed organization features enabled by the pinned server default; no plan override is assumed.
|
|
370
|
+
const DEFAULT_FEATURES = [
|
|
371
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:advanced-search"
|
|
372
|
+
"advanced-search",
|
|
373
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:change-alerts"
|
|
374
|
+
"change-alerts",
|
|
375
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:crash-rate-alerts"
|
|
376
|
+
"crash-rate-alerts",
|
|
377
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:custom-symbol-sources"
|
|
378
|
+
"custom-symbol-sources",
|
|
379
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/temporary.py "organizations:dashboards-basic"
|
|
380
|
+
"dashboards-basic",
|
|
381
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/temporary.py "organizations:dashboards-edit"
|
|
382
|
+
"dashboards-edit",
|
|
383
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:data-forwarding"
|
|
384
|
+
"data-forwarding",
|
|
385
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:discover-basic"
|
|
386
|
+
"discover-basic",
|
|
387
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:discover-query"
|
|
388
|
+
"discover-query",
|
|
389
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:event-attachments"
|
|
390
|
+
"event-attachments",
|
|
391
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/temporary.py "organizations:insight-modules"
|
|
392
|
+
"insight-modules",
|
|
393
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:integrations-alert-rule"
|
|
394
|
+
"integrations-alert-rule",
|
|
395
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:integrations-chat-unfurl"
|
|
396
|
+
"integrations-chat-unfurl",
|
|
397
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:integrations-codeowners"
|
|
398
|
+
"integrations-codeowners",
|
|
399
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:integrations-enterprise-alert-rule"
|
|
400
|
+
"integrations-enterprise-alert-rule",
|
|
401
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:integrations-enterprise-incident-management"
|
|
402
|
+
"integrations-enterprise-incident-management",
|
|
403
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:integrations-event-hooks"
|
|
404
|
+
"integrations-event-hooks",
|
|
405
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:integrations-incident-management"
|
|
406
|
+
"integrations-incident-management",
|
|
407
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:integrations-issue-basic"
|
|
408
|
+
"integrations-issue-basic",
|
|
409
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:integrations-issue-sync"
|
|
410
|
+
"integrations-issue-sync",
|
|
411
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:integrations-scm-multi-org"
|
|
412
|
+
"integrations-scm-multi-org",
|
|
413
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:integrations-stacktrace-link"
|
|
414
|
+
"integrations-stacktrace-link",
|
|
415
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:integrations-ticket-rules"
|
|
416
|
+
"integrations-ticket-rules",
|
|
417
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:integrations-vercel"
|
|
418
|
+
"integrations-vercel",
|
|
419
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/temporary.py "organizations:invite-members"
|
|
420
|
+
"invite-members",
|
|
421
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:performance-view"
|
|
422
|
+
"performance-view",
|
|
423
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:relay"
|
|
424
|
+
"relay",
|
|
425
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/temporary.py "organizations:sentry-app-schema-form-migration"
|
|
426
|
+
"sentry-app-schema-form-migration",
|
|
427
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/temporary.py "organizations:session-replay-ui"
|
|
428
|
+
"session-replay-ui",
|
|
429
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:sso-basic"
|
|
430
|
+
"sso-basic",
|
|
431
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:sso-saml2"
|
|
432
|
+
"sso-saml2",
|
|
433
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:team-insights"
|
|
434
|
+
"team-insights",
|
|
435
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:team-roles"
|
|
436
|
+
"team-roles",
|
|
437
|
+
// source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/features/permanent.py "organizations:uptime"
|
|
438
|
+
"uptime",
|
|
439
|
+
];
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// FETCH-FIRST (runtime contract R12b): a fixed file, the kernel's serve seam around the pack's fetch (./fetch.ts).
|
|
2
|
+
import { serveHttp, type PackFetchOptions } from '@volter/world-core';
|
|
3
|
+
import { createSentryFetch } from './fetch.ts';
|
|
4
|
+
|
|
5
|
+
export async function createSentryTwinServer(options: PackFetchOptions & { port?: number } = {}): Promise<{ port: number; stop: () => void }> {
|
|
6
|
+
const server = await serveHttp({ port: options.port ?? 0, idleTimeout: 60, fetch: createSentryFetch(options) });
|
|
7
|
+
return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
|
|
8
|
+
}
|
package/client/sentry-mirror.css
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
:root {
|
|
2
|
-
--bg: #1d1127;
|
|
3
|
-
--panel: #251a35;
|
|
4
|
-
--panel-2: #2e2140;
|
|
5
|
-
--border: #3a2d50;
|
|
6
|
-
--text: #ebe6f2;
|
|
7
|
-
--muted: #a094b3;
|
|
8
|
-
--accent: #6c5fc7;
|
|
9
|
-
--fatal: #c84b48;
|
|
10
|
-
--error: #e4774e;
|
|
11
|
-
--warning: #e2c14f;
|
|
12
|
-
--info: #5b9bd5;
|
|
13
|
-
--debug: #8a7fb0;
|
|
14
|
-
--resolved: #3aa76d;
|
|
15
|
-
--ignored: #6b5f82;
|
|
16
|
-
}
|
|
17
|
-
* { box-sizing: border-box; }
|
|
18
|
-
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: var(--bg); color: var(--text); }
|
|
19
|
-
.mono { font-family: "SFMono-Regular", Menlo, Consolas, monospace; }
|
|
20
|
-
.app { display: flex; flex-direction: column; height: 100vh; }
|
|
21
|
-
.topbar { display: flex; align-items: center; gap: 16px; padding: 0 18px; height: 52px; background: var(--panel); border-bottom: 1px solid var(--border); }
|
|
22
|
-
.brand { font-weight: 700; font-size: 16px; }
|
|
23
|
-
.badge { font-size: 11px; background: var(--accent); padding: 2px 6px; border-radius: 4px; vertical-align: middle; }
|
|
24
|
-
.org { color: var(--muted); font-size: 13px; }
|
|
25
|
-
.body { display: flex; flex: 1; min-height: 0; }
|
|
26
|
-
.sidenav { width: 180px; background: var(--panel); border-right: 1px solid var(--border); padding: 12px 8px; display: flex; flex-direction: column; gap: 4px; }
|
|
27
|
-
.nav-item { text-align: left; background: none; border: none; color: var(--muted); padding: 8px 12px; border-radius: 6px; cursor: pointer; font-size: 14px; }
|
|
28
|
-
.nav-item:hover { background: var(--panel-2); color: var(--text); }
|
|
29
|
-
.nav-item.active { background: var(--accent); color: #fff; }
|
|
30
|
-
.content { flex: 1; min-width: 0; overflow: hidden; display: flex; }
|
|
31
|
-
.split { display: flex; flex: 1; min-width: 0; }
|
|
32
|
-
.list { width: 480px; border-right: 1px solid var(--border); overflow-y: auto; }
|
|
33
|
-
.list.full { width: 100%; padding: 18px; }
|
|
34
|
-
.detail { flex: 1; overflow-y: auto; padding: 22px; }
|
|
35
|
-
.search-bar { padding: 12px; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--bg); }
|
|
36
|
-
.search-input { width: 100%; padding: 8px 10px; background: var(--panel-2); border: 1px solid var(--border); border-radius: 6px; color: var(--text); font-family: monospace; }
|
|
37
|
-
.issue-row { display: flex; justify-content: space-between; align-items: center; width: 100%; text-align: left; background: none; border: none; border-bottom: 1px solid var(--border); color: var(--text); padding: 12px 14px; cursor: pointer; }
|
|
38
|
-
.issue-row:hover { background: var(--panel-2); }
|
|
39
|
-
.issue-row.selected { background: var(--panel-2); border-left: 3px solid var(--accent); }
|
|
40
|
-
.issue-title { font-weight: 600; font-size: 14px; }
|
|
41
|
-
.issue-culprit { color: var(--muted); font-size: 12px; margin-top: 2px; }
|
|
42
|
-
.issue-meta { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
|
|
43
|
-
.issue-count, .issue-lastseen { color: var(--muted); font-size: 12px; }
|
|
44
|
-
.pill { font-size: 11px; padding: 2px 7px; border-radius: 10px; text-transform: uppercase; font-weight: 600; }
|
|
45
|
-
.level-fatal { background: var(--fatal); color: #fff; }
|
|
46
|
-
.level-error { background: var(--error); color: #fff; }
|
|
47
|
-
.level-warning { background: var(--warning); color: #1d1127; }
|
|
48
|
-
.level-info { background: var(--info); color: #fff; }
|
|
49
|
-
.level-debug, .level-muted { background: var(--debug); color: #fff; }
|
|
50
|
-
.status-unresolved { background: #43354f; color: var(--text); }
|
|
51
|
-
.status-resolved { background: var(--resolved); color: #fff; }
|
|
52
|
-
.status-ignored, .status-muted { background: var(--ignored); color: #fff; }
|
|
53
|
-
.detail-title { margin: 0 0 4px; font-size: 20px; }
|
|
54
|
-
.detail-sub { color: var(--muted); margin-bottom: 16px; }
|
|
55
|
-
.detail-stats { display: flex; gap: 26px; margin-bottom: 22px; }
|
|
56
|
-
.detail-stats .stat-label { display: block; color: var(--muted); font-size: 11px; text-transform: uppercase; }
|
|
57
|
-
.detail-stats .stat-value { font-size: 18px; font-weight: 600; }
|
|
58
|
-
.stacktrace { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
|
|
59
|
-
.frame { display: flex; justify-content: space-between; padding: 7px 12px; border-bottom: 1px solid var(--border); font-size: 12px; }
|
|
60
|
-
.frame:last-child { border-bottom: none; }
|
|
61
|
-
.frame-fn { color: var(--accent); }
|
|
62
|
-
.frame-file { color: var(--muted); }
|
|
63
|
-
.tags { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
64
|
-
.tag { background: var(--panel); border: 1px solid var(--border); padding: 4px 8px; border-radius: 4px; font-size: 12px; }
|
|
65
|
-
.empty { color: var(--muted); padding: 24px; text-align: center; }
|
|
66
|
-
.screen-title { margin: 0 0 16px; }
|
|
67
|
-
.release-row, .project-row { display: flex; gap: 18px; align-items: center; padding: 12px 14px; border-bottom: 1px solid var(--border); }
|
|
68
|
-
.release-version, .project-name { font-weight: 600; min-width: 200px; }
|
|
69
|
-
.release-commits, .release-date, .project-slug, .project-platform { color: var(--muted); font-size: 13px; }
|
|
70
|
-
h3 { margin: 22px 0 10px; font-size: 14px; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
|
|
71
|
-
.alerts-sub { color: var(--muted); font-size: 13px; margin-bottom: 14px; }
|
|
72
|
-
.alert-rule-row { display: flex; gap: 18px; align-items: center; padding: 12px 14px; border-bottom: 1px solid var(--border); }
|
|
73
|
-
.alert-rule-name { font-weight: 600; min-width: 200px; }
|
|
74
|
-
.alert-rule-triggers { display: flex; gap: 6px; }
|
|
75
|
-
.alert-trigger { background: #43354f; color: var(--text); }
|
|
76
|
-
.alert-rule-endpoints { color: var(--muted); font-size: 13px; }
|
|
77
|
-
.settings-section { margin-bottom: 24px; }
|
|
78
|
-
.setting-field, .dsn-row { display: flex; gap: 18px; align-items: center; padding: 8px 14px; border-bottom: 1px solid var(--border); }
|
|
79
|
-
.setting-label, .dsn-name { color: var(--muted); min-width: 120px; }
|
|
80
|
-
.setting-value, .dsn-public { font-weight: 500; }
|
|
81
|
-
.dsn-public { color: var(--accent); font-size: 12px; }
|
|
82
|
-
.env-list { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
83
|
-
.env-item { background: var(--panel); border: 1px solid var(--border); color: var(--text); }
|
|
84
|
-
.dashboards-sub, .discover-results-note { color: var(--muted); font-size: 13px; margin-bottom: 14px; }
|
|
85
|
-
.dashboard-row { padding: 14px; border-bottom: 1px solid var(--border); }
|
|
86
|
-
.dashboard-title { font-weight: 600; font-size: 15px; display: block; margin-bottom: 10px; }
|
|
87
|
-
.dashboard-widgets { display: flex; flex-wrap: wrap; gap: 10px; }
|
|
88
|
-
.widget-card { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; min-width: 160px; background: #1c1530; border: 1px solid var(--border); border-radius: 8px; }
|
|
89
|
-
.widget-title { font-weight: 500; }
|
|
90
|
-
.widget-type { background: #43354f; color: var(--text); align-self: flex-start; }
|
|
91
|
-
.discover-builder { display: flex; flex-direction: column; gap: 12px; padding: 14px; background: #1c1530; border: 1px solid var(--border); border-radius: 8px; margin-bottom: 14px; }
|
|
92
|
-
.discover-field { display: flex; flex-direction: column; gap: 6px; }
|
|
93
|
-
.discover-label { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .04em; }
|
|
94
|
-
.discover-columns, .discover-query { padding: 8px 12px; border-radius: 6px; border: 1px solid var(--border); background: #120c22; color: var(--text); font: inherit; }
|
|
95
|
-
.discover-columns-preview { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
96
|
-
.discover-column-chip { padding: 4px 8px; background: #2a2140; border-radius: 5px; font-size: 12px; }
|
|
97
|
-
.saved-query-row { display: flex; gap: 18px; align-items: center; padding: 10px 14px; border-bottom: 1px solid var(--border); }
|
|
98
|
-
.saved-query-name { font-weight: 600; min-width: 180px; }
|
|
99
|
-
.saved-query-fields { color: var(--muted); font-size: 12px; }
|