@volter/twin-sentry 0.1.37 → 1.0.0

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.
Files changed (188) hide show
  1. package/README.md +21 -134
  2. package/api/spec/SOURCE.md +15 -0
  3. package/api/spec/doc-examples.json +12 -0
  4. package/api/spec/openapi.json.gz +0 -0
  5. package/api/spec/patches.json +5350 -0
  6. package/api/spec/sources.json +122 -0
  7. package/api/src/fetch.ts +18 -0
  8. package/api/src/generated/surface.gen.json +1 -0
  9. package/api/src/generated/ui.gen.json +1 -0
  10. package/api/src/index.ts +3 -0
  11. package/api/src/manifest.ts +136 -0
  12. package/api/src/semantics/api.ts +246 -0
  13. package/api/src/semantics/around.ts +36 -0
  14. package/api/src/semantics/doors.ts +50 -0
  15. package/api/src/semantics/index.ts +19 -0
  16. package/api/src/semantics/shared.ts +35 -0
  17. package/api/src/semantics/states.ts +17 -0
  18. package/dist/api/spec/SOURCE.md +15 -0
  19. package/dist/api/spec/doc-examples.json +12 -0
  20. package/dist/api/spec/openapi.json.gz +0 -0
  21. package/dist/api/spec/patches.json +5350 -0
  22. package/dist/api/spec/sources.json +122 -0
  23. package/dist/api/src/fetch.d.ts +2 -0
  24. package/dist/api/src/fetch.js +17 -0
  25. package/dist/api/src/fetch.ts +18 -0
  26. package/dist/api/src/generated/surface.gen.json +1 -0
  27. package/dist/api/src/generated/ui.gen.json +1 -0
  28. package/dist/api/src/index.d.ts +2 -0
  29. package/dist/api/src/index.js +3 -0
  30. package/dist/api/src/index.ts +3 -0
  31. package/dist/api/src/manifest.d.ts +2 -0
  32. package/dist/api/src/manifest.js +135 -0
  33. package/dist/api/src/manifest.ts +136 -0
  34. package/dist/api/src/semantics/api.d.ts +13 -0
  35. package/dist/api/src/semantics/api.js +267 -0
  36. package/dist/api/src/semantics/api.ts +246 -0
  37. package/dist/api/src/semantics/around.d.ts +2 -0
  38. package/dist/api/src/semantics/around.js +41 -0
  39. package/dist/api/src/semantics/around.ts +36 -0
  40. package/dist/api/src/semantics/doors.d.ts +8 -0
  41. package/dist/api/src/semantics/doors.js +39 -0
  42. package/dist/api/src/semantics/doors.ts +50 -0
  43. package/dist/api/src/semantics/index.d.ts +2 -0
  44. package/dist/api/src/semantics/index.js +15 -0
  45. package/dist/api/src/semantics/index.ts +19 -0
  46. package/dist/api/src/semantics/shared.d.ts +15 -0
  47. package/dist/api/src/semantics/shared.js +27 -0
  48. package/dist/api/src/semantics/shared.ts +35 -0
  49. package/dist/api/src/semantics/states.d.ts +24 -0
  50. package/dist/api/src/semantics/states.js +17 -0
  51. package/dist/api/src/semantics/states.ts +17 -0
  52. package/dist/generated/pack-facts.json +87 -0
  53. package/dist/ingest/spec/SOURCE.md +30 -0
  54. package/dist/ingest/spec/authentication.mdx.gz +0 -0
  55. package/dist/ingest/spec/compression.mdx.gz +0 -0
  56. package/dist/ingest/spec/doc-examples.json +216 -0
  57. package/dist/ingest/spec/envelope-items.mdx.gz +0 -0
  58. package/dist/ingest/spec/envelopes.mdx.gz +0 -0
  59. package/dist/ingest/spec/openapi.json +178 -0
  60. package/dist/ingest/spec/rate-limiting.mdx.gz +0 -0
  61. package/dist/ingest/spec/sources.json +25 -0
  62. package/dist/ingest/spec/transport.mdx.gz +0 -0
  63. package/dist/ingest/spec/write-spec.ts +192 -0
  64. package/dist/ingest/src/fetch.d.ts +2 -0
  65. package/dist/ingest/src/fetch.js +15 -0
  66. package/dist/ingest/src/fetch.ts +16 -0
  67. package/dist/ingest/src/generated/surface.gen.json +1 -0
  68. package/dist/ingest/src/generated/ui.gen.json +1 -0
  69. package/dist/ingest/src/index.d.ts +2 -0
  70. package/dist/ingest/src/index.js +3 -0
  71. package/dist/ingest/src/index.ts +3 -0
  72. package/dist/ingest/src/manifest.d.ts +2 -0
  73. package/dist/ingest/src/manifest.js +34 -0
  74. package/dist/ingest/src/manifest.ts +39 -0
  75. package/dist/ingest/src/semantics/api.d.ts +2 -0
  76. package/dist/ingest/src/semantics/api.js +240 -0
  77. package/dist/ingest/src/semantics/api.ts +188 -0
  78. package/dist/ingest/src/semantics/around.d.ts +2 -0
  79. package/dist/ingest/src/semantics/around.js +6 -0
  80. package/dist/ingest/src/semantics/around.ts +7 -0
  81. package/dist/ingest/src/semantics/index.d.ts +2 -0
  82. package/dist/ingest/src/semantics/index.js +4 -0
  83. package/dist/ingest/src/semantics/index.ts +8 -0
  84. package/dist/ingest/src/semantics/shared.d.ts +4 -0
  85. package/dist/ingest/src/semantics/shared.js +2 -0
  86. package/dist/ingest/src/semantics/shared.ts +7 -0
  87. package/dist/ingest/src/semantics/states.d.ts +1 -0
  88. package/dist/ingest/src/semantics/states.js +2 -0
  89. package/dist/ingest/src/semantics/states.ts +2 -0
  90. package/dist/journeys/coverage.json +205 -0
  91. package/dist/journeys/customer-life.json +923 -0
  92. package/dist/journeys/customer-life.outline.md +51 -0
  93. package/dist/journeys/decisions.json +170 -0
  94. package/dist/journeys/demand.json +136 -0
  95. package/dist/journeys/score.json +231 -0
  96. package/dist/journeys/unreachable.json +29 -0
  97. package/dist/journeys/vendor-examples.json +474 -0
  98. package/dist/spec/sources.json +76 -0
  99. package/dist/src/cli.js +12 -27
  100. package/dist/src/fetch.d.ts +2 -0
  101. package/dist/src/fetch.js +9 -0
  102. package/dist/src/index.d.ts +5 -15
  103. package/dist/src/index.js +12 -75
  104. package/dist/src/manifest.d.ts +2 -0
  105. package/dist/src/manifest.js +34 -0
  106. package/dist/src/semantics/shared.d.ts +87 -0
  107. package/dist/src/semantics/shared.js +411 -0
  108. package/dist/src/server.d.ts +7 -0
  109. package/dist/src/server.js +7 -0
  110. package/generated/pack-facts.json +87 -0
  111. package/ingest/spec/SOURCE.md +30 -0
  112. package/ingest/spec/authentication.mdx.gz +0 -0
  113. package/ingest/spec/compression.mdx.gz +0 -0
  114. package/ingest/spec/doc-examples.json +216 -0
  115. package/ingest/spec/envelope-items.mdx.gz +0 -0
  116. package/ingest/spec/envelopes.mdx.gz +0 -0
  117. package/ingest/spec/openapi.json +178 -0
  118. package/ingest/spec/rate-limiting.mdx.gz +0 -0
  119. package/ingest/spec/sources.json +25 -0
  120. package/ingest/spec/transport.mdx.gz +0 -0
  121. package/ingest/spec/write-spec.ts +192 -0
  122. package/ingest/src/fetch.ts +16 -0
  123. package/ingest/src/generated/surface.gen.json +1 -0
  124. package/ingest/src/generated/ui.gen.json +1 -0
  125. package/ingest/src/index.ts +3 -0
  126. package/ingest/src/manifest.ts +39 -0
  127. package/ingest/src/semantics/api.ts +188 -0
  128. package/ingest/src/semantics/around.ts +7 -0
  129. package/ingest/src/semantics/index.ts +8 -0
  130. package/ingest/src/semantics/shared.ts +7 -0
  131. package/ingest/src/semantics/states.ts +2 -0
  132. package/journeys/coverage.json +205 -0
  133. package/journeys/customer-life.json +923 -0
  134. package/journeys/customer-life.outline.md +51 -0
  135. package/journeys/decisions.json +170 -0
  136. package/journeys/demand.json +136 -0
  137. package/journeys/score.json +231 -0
  138. package/journeys/unreachable.json +29 -0
  139. package/journeys/vendor-examples.json +474 -0
  140. package/package.json +16 -42
  141. package/spec/sources.json +76 -0
  142. package/src/cli.ts +12 -24
  143. package/src/fetch.ts +10 -0
  144. package/src/index.ts +12 -143
  145. package/src/manifest.ts +40 -0
  146. package/src/semantics/shared.ts +443 -0
  147. package/src/server.ts +8 -0
  148. package/client/sentry-mirror.css +0 -99
  149. package/client/sentry-mirror.tsx +0 -352
  150. package/dist/client/sentry-mirror.bundle.js +0 -388
  151. package/dist/client/sentry-mirror.css +0 -99
  152. package/dist/client/sentry-mirror.d.ts +0 -17
  153. package/dist/client/sentry-mirror.js +0 -156
  154. package/dist/client/sentry-mirror.tsx +0 -352
  155. package/dist/src/sentry-budget.d.ts +0 -50
  156. package/dist/src/sentry-budget.js +0 -145
  157. package/dist/src/sentry-capabilities.d.ts +0 -3
  158. package/dist/src/sentry-capabilities.js +0 -1180
  159. package/dist/src/sentry-conformance.d.ts +0 -22
  160. package/dist/src/sentry-conformance.js +0 -97
  161. package/dist/src/sentry-connector.d.ts +0 -77
  162. package/dist/src/sentry-connector.js +0 -226
  163. package/dist/src/sentry-events.d.ts +0 -54
  164. package/dist/src/sentry-events.js +0 -132
  165. package/dist/src/sentry-ingest.d.ts +0 -137
  166. package/dist/src/sentry-ingest.js +0 -387
  167. package/dist/src/sentry-mirror-ui.d.ts +0 -38
  168. package/dist/src/sentry-mirror-ui.js +0 -162
  169. package/dist/src/sentry-perform-harness.d.ts +0 -9
  170. package/dist/src/sentry-perform-harness.js +0 -20
  171. package/dist/src/sentry-server.d.ts +0 -14
  172. package/dist/src/sentry-server.js +0 -27
  173. package/dist/src/sentry-twin.d.ts +0 -17
  174. package/dist/src/sentry-twin.js +0 -1739
  175. package/dist/test-fixtures/sentry-openapi-operations.SOURCE.md +0 -24
  176. package/dist/test-fixtures/sentry-openapi-operations.json +0 -1837
  177. package/src/sentry-budget.ts +0 -171
  178. package/src/sentry-capabilities.ts +0 -1222
  179. package/src/sentry-conformance.ts +0 -110
  180. package/src/sentry-connector.ts +0 -260
  181. package/src/sentry-events.ts +0 -172
  182. package/src/sentry-ingest.ts +0 -471
  183. package/src/sentry-mirror-ui.ts +0 -162
  184. package/src/sentry-perform-harness.ts +0 -19
  185. package/src/sentry-server.ts +0 -35
  186. package/src/sentry-twin.ts +0 -1615
  187. package/test-fixtures/sentry-openapi-operations.SOURCE.md +0 -24
  188. package/test-fixtures/sentry-openapi-operations.json +0 -1837
@@ -0,0 +1,39 @@
1
+ // The ingest lane's manifest: the vendor facts its spec (../spec, written from Sentry's SDK developer docs) does not
2
+ // carry. Every SDK sends what it captures here, as envelopes (./semantics/api.ts); the envelope authenticates itself
3
+ // by its DSN's public key, so there is no manifest `auth`.
4
+ import type { DerivedManifest } from '@volter/world-core';
5
+
6
+ export const manifest: DerivedManifest = {
7
+ vendor: 'sentry',
8
+ // one service with the API lane (../../api/src/manifest.ts), so one store
9
+ service: 'sentry',
10
+ // an envelope is not JSON: its bytes are kept (./semantics/around.ts) and read by the handler
11
+ body: {},
12
+ // source: https://raw.githubusercontent.com/getsentry/sentry-docs/3ec4e139711af82b38a3c2c17261f5f5cdb66dfc/develop-docs/sdk/foundations/transport/compression.mdx "The following content encodings are accepted by Relay and Sentry:"
13
+ // The kernel decodes the declared vendor encodings before the handler reads the retained bytes.
14
+ encodings: { request: ['gzip', 'deflate', 'br', 'zstd'], undecodable: { status: 400, message: 'failed to read request body' } },
15
+ ids: { template: '{uuid}' },
16
+ time: 'iso',
17
+ // Relay's error body: `{"detail": ..., "causes": [...]}` (develop-docs/sdk/foundations/transport/index.mdx)
18
+ error: { detail: '{message}' },
19
+ readOnly: { status: 403, message: 'This twin was started read-only; envelopes are refused.' },
20
+ malformedBody: { status: 400, message: 'failed to read request body' },
21
+ notFound: { status: 404, message: 'Not Found' },
22
+ gap: { status: 404, message: 'Not Found' },
23
+ deleted: {},
24
+ // a browser SDK posts cross-origin: every answer allows any origin and exposes what an SDK reads of it
25
+ // (transport/rate-limiting.mdx: Retry-After, X-Sentry-Rate-Limits). Where the documentation stops: Relay's CORS is its
26
+ // code's, not a page's; the headers allowed are the ones the SDKs' transports send
27
+ cors: {
28
+ origin: '*',
29
+ methods: 'GET, POST, OPTIONS',
30
+ headers: 'x-sentry-auth, x-requested-with, x-forwarded-for, origin, referer, accept, content-type, authentication, authorization, content-encoding, transfer-encoding, sentry-trace, baggage',
31
+ expose: 'x-sentry-error, x-sentry-rate-limits, retry-after',
32
+ maxAge: 3600,
33
+ },
34
+ // the sequences the vendor numbers by (ctx.issue over each, src/semantics/shared.ts next)
35
+ // All stored vendor resources are owned and refreshed by the API manifest through ctx.over.
36
+ resources: {},
37
+ vendorBacked: { none: 'Envelope receipt is telemetry ingestion, never deployed as account changes. The API manifest owns and refreshes its resulting vendor resources.' },
38
+ discovery: { twinOf: "Sentry's ingestion endpoint (Relay): the envelopes every SDK sends", stores: 'events and their issues, transactions, sessions, client reports, attachments, replays, profiles, cron check-ins and monitors, logs, metrics and spans' },
39
+ };
@@ -0,0 +1,188 @@
1
+ // Sentry's ingestion endpoint (spec: sendEnvelope; develop-docs/sdk/foundations/envelopes/index.mdx): an envelope, its
2
+ // header line and its items, each kept as what it is. The envelope is authenticated by the DSN's public key (the
3
+ // X-Sentry-Auth header, the `sentry_key` query parameter, or the envelope header's `dsn`), and answered with its event id.
4
+ import { s3, type HandlerContext } from '@volter/world-core';
5
+ import { apiManifest, ATTACHMENT, CHECK_IN, CLIENT_REPORT, keepEvent, LOG, METRIC, MONITOR, next, OTHER_ITEM, PROFILE, projectByKey, REPLAY, type Row, SESSION } from './shared.ts';
6
+
7
+ const obj = (v: unknown): Row => (v && typeof v === 'object' && !Array.isArray(v) ? v as Row : {});
8
+ const refuse = (status: number, detail: string): Response =>
9
+ Response.json({ detail }, { status, headers: { 'x-sentry-error': detail } });
10
+
11
+ // source: https://raw.githubusercontent.com/getsentry/sentry-docs/3ec4e139711af82b38a3c2c17261f5f5cdb66dfc/develop-docs/sdk/foundations/envelopes/index.mdx "*200 MiB* for an envelope after decompression including all envelope items."
12
+ const MAX_ENVELOPE = 200 * 1024 * 1024;
13
+
14
+ /** The body's bytes: the kernel has decoded a gzip, deflate, br or zstd body (the manifest's `encodings`) and dropped the
15
+ * header, so a Content-Encoding still on the request is one Sentry does not read. */
16
+ function decoded(bytes: Uint8Array, encoding: string | null): Uint8Array | string {
17
+ const coding = (encoding ?? '').toLowerCase();
18
+ return coding === '' || coding === 'identity' ? bytes : `unsupported content encoding: ${encoding}`;
19
+ }
20
+
21
+ type Item = { header: Row; payload: Uint8Array };
22
+
23
+ /** An envelope read: its header and items. An item's header may give its `length`, else it runs to the newline
24
+ * (develop-docs/sdk/foundations/envelopes/index.mdx, "Envelopes" and "Items"). */
25
+ function parse(bytes: Uint8Array): { header: Row; items: Item[] } | string {
26
+ let at = 0;
27
+ const line = (): Uint8Array | undefined => {
28
+ if (at >= bytes.length) return undefined;
29
+ let end = bytes.indexOf(10, at);
30
+ if (end < 0) end = bytes.length;
31
+ const out = bytes.subarray(at, end);
32
+ at = end + 1;
33
+ return out;
34
+ };
35
+ const json = (b: Uint8Array | undefined): Row | undefined => {
36
+ if (!b) return undefined;
37
+ try { const v = JSON.parse(new TextDecoder().decode(b)); return v && typeof v === 'object' && !Array.isArray(v) ? v : undefined; } catch { return undefined; }
38
+ };
39
+ const header = json(line());
40
+ if (!header) return 'invalid envelope header';
41
+ const items: Item[] = [];
42
+ while (at < bytes.length) {
43
+ const raw = line();
44
+ if (!raw || raw.length === 0 || (raw.length === 1 && raw[0] === 13)) continue;
45
+ const h = json(raw);
46
+ if (!h || typeof h.type !== 'string') return 'invalid item header';
47
+ let payload: Uint8Array;
48
+ if (typeof h.length === 'number') {
49
+ if (!Number.isInteger(h.length) || h.length < 0) return 'invalid item length';
50
+ if (at + h.length > bytes.length) return 'item payload is shorter than its length';
51
+ payload = bytes.subarray(at, at + h.length);
52
+ at += h.length;
53
+ if (at < bytes.length && bytes[at] !== 10) return 'length-prefixed payload must end with newline or EOF';
54
+ if (bytes[at] === 10) at += 1;
55
+ } else payload = line() ?? new Uint8Array(0);
56
+ items.push({ header: h, payload });
57
+ }
58
+ return { header, items };
59
+ }
60
+
61
+ /** The public key a request authenticates with: X-Sentry-Auth's `sentry_key`, the query's, or the envelope's DSN. */
62
+ function publicKeyOf(request: Request, header: Row): string | undefined {
63
+ const auth = request.headers.get('x-sentry-auth') ?? '';
64
+ const fromHeader = /sentry_key=([^,\s]+)/.exec(auth)?.[1];
65
+ const fromQuery = new URL(request.url).searchParams.get('sentry_key') ?? undefined;
66
+ let fromDsn: string | undefined;
67
+ if (typeof header.dsn === 'string') { try { fromDsn = new URL(header.dsn).username || undefined; } catch { /* not a URL */ } }
68
+ const given = [fromHeader, fromQuery, fromDsn].filter((k): k is string => !!k);
69
+ // "If multiple forms of authentication are given, the endpoint validates that the information matches and otherwise
70
+ // rejects the request" (envelopes/index.mdx, Authentication)
71
+ return given.length && given.every((k) => k === given[0]) ? given[0] : given.length ? '' : undefined;
72
+ }
73
+
74
+ const text = (b: Uint8Array): string => new TextDecoder().decode(b);
75
+ const asJson = (b: Uint8Array): Row => { try { return obj(JSON.parse(text(b))); } catch { return {}; } };
76
+
77
+ // source: spec:sendEnvelope "The main ingestion endpoint for Envelopes."
78
+ export async function sendEnvelope(ctx: HandlerContext): Promise<Response> {
79
+ const request = ctx.call.request;
80
+ const body = decoded(s3.bytesOf(request), request.headers.get('content-encoding'));
81
+ ctx = await ctx.over(apiManifest);
82
+ if (typeof body === 'string') return refuse(400, body);
83
+ if (body.length > MAX_ENVELOPE) return refuse(413, 'envelope exceeded size limits');
84
+ const envelope = parse(body);
85
+ if (typeof envelope === 'string') return refuse(400, `invalid envelope: ${envelope}`);
86
+ const key = publicKeyOf(request, envelope.header);
87
+ // "If both are missing, the Envelope is rejected with status code `403 Forbidden`." (envelopes/index.mdx)
88
+ if (key === undefined) return refuse(403, 'missing authorization information');
89
+ if (key === '') return refuse(403, 'the authentication given does not match');
90
+ // Where the documentation stops: Relay's answer for a key no project has is not a page's; the twin refuses it as
91
+ // missing authentication is refused, and a key of another project than the path's likewise
92
+ const project = projectByKey(ctx, key);
93
+ if (!project || String(project.id) !== String(ctx.call.params.project_id)) return refuse(403, 'invalid api key');
94
+ const org = obj(ctx.row('organization', String(project._organization)));
95
+ const envelopeId = typeof envelope.header.event_id === 'string' ? envelope.header.event_id.replace(/-/g, '') : undefined;
96
+ const sentAt = typeof envelope.header.sent_at === 'string' ? envelope.header.sent_at : ctx.occurredAt;
97
+ const base = { project: project.id, organization: org.id ?? project._organization, received: ctx.occurredAt, sent_at: sentAt };
98
+ // source: https://raw.githubusercontent.com/getsentry/sentry-docs/3ec4e139711af82b38a3c2c17261f5f5cdb66dfc/develop-docs/sdk/foundations/envelopes/index.mdx "*1 MiB* for event (errors and transactions), span, log, and metric (statsd, buckets, meta) envelope items."
99
+ let sessions = 0;
100
+ for (const item of envelope.items) {
101
+ const kind = String(item.header.type);
102
+ const limit = ({ event: 1048576, transaction: 1048576, span: 1048576, log: 1048576, trace_metric: 1048576, statsd: 1048576, metric_buckets: 1048576, metric_meta: 1048576, check_in: 102400, client_report: 4096, profile: 52428800, replay_event: 104857600, replay_recording: 10485760, replay_video: 10485760 } as Record<string, number>)[kind];
103
+ if (limit && item.payload.length > limit) return refuse(413, 'item exceeded size limits');
104
+ const payload = asJson(item.payload);
105
+ if (kind === 'session') sessions++;
106
+ if (sessions > 100 || (kind === 'sessions' && Array.isArray(payload.aggregates) && payload.aggregates.length > 100)) return refuse(413, 'too many sessions');
107
+ if (kind === 'trace_metric' && Array.isArray(payload.items) && payload.items.some((metric) => new TextEncoder().encode(JSON.stringify(metric)).length > 2048)) return refuse(413, 'metric exceeded size limits');
108
+ }
109
+ let n = 0;
110
+ const envelopeN = await next(ctx, 'envelope');
111
+ const receipt = `envelope/${String(project.id)}/${envelopeN}`;
112
+ await ctx.blobs.put(receipt, body);
113
+ await ctx.record('_envelope', { ...base, header: envelope.header, blob: receipt, size: body.length }, `${String(project.id)}:${envelopeN}`);
114
+ const id = (kind: string): string => `${String(project.id)}:${kind}:${envelopeN}:${n++}`;
115
+ for (const item of envelope.items) {
116
+ const type = String(item.header.type);
117
+ const p = asJson(item.payload);
118
+ switch (type) {
119
+ case 'event': await keepEvent(ctx, project, { ...p, event_id: envelopeId ?? p.event_id }, p.exception ? 'error' : 'default'); break;
120
+ case 'transaction': await keepEvent(ctx, project, { ...p, event_id: envelopeId ?? p.event_id }, 'transaction'); break;
121
+ case 'feedback': await keepEvent(ctx, project, { ...p, event_id: envelopeId ?? p.event_id }, 'feedback'); break;
122
+ case 'attachment': {
123
+ const blob = `attachment/${String(project.id)}/${envelopeId ?? 'none'}/${String(item.header.filename ?? n)}`;
124
+ await ctx.blobs.put(blob, item.payload);
125
+ await ctx.record(ATTACHMENT, { ...base, event_id: envelopeId ?? null, name: item.header.filename ?? null, type: item.header.attachment_type ?? 'event.attachment', mimetype: item.header.content_type ?? 'application/octet-stream', size: item.payload.length, blob }, id(ATTACHMENT));
126
+ break;
127
+ }
128
+ case 'session': await ctx.record(SESSION, { ...base, sid: p.sid ?? null, did: p.did ?? null, status: p.status ?? 'ok', errors: p.errors ?? 0, init: p.init ?? false, started: p.started ?? null, duration: p.duration ?? null, attrs: obj(p.attrs) }, `${String(project.id)}:session:${String(p.sid ?? id(SESSION))}`); break;
129
+ case 'sessions': await ctx.record(SESSION, { ...base, aggregates: p.aggregates ?? [], attrs: obj(p.attrs) }, id(SESSION)); break;
130
+ case 'client_report': await ctx.record(CLIENT_REPORT, { ...base, timestamp: p.timestamp ?? null, discarded_events: p.discarded_events ?? [] }, id(CLIENT_REPORT)); break;
131
+ case 'replay_event': await ctx.record(REPLAY, { ...base, replay_id: p.replay_id ?? envelopeId ?? null, segment_id: p.segment_id ?? 0, replay_type: p.replay_type ?? null, urls: p.urls ?? [], error_ids: p.error_ids ?? [], trace_ids: p.trace_ids ?? [], timestamp: p.timestamp ?? null }, id(REPLAY)); break;
132
+ case 'replay_recording': case 'replay_video': {
133
+ const blob = `replay/${String(project.id)}/${envelopeId ?? 'none'}/${n}`;
134
+ await ctx.blobs.put(blob, item.payload);
135
+ await ctx.record(REPLAY, { ...base, replay_id: envelopeId ?? null, recording: blob, kind: type, size: item.payload.length }, id(REPLAY));
136
+ break;
137
+ }
138
+ case 'profile': case 'profile_chunk': {
139
+ const blob = `profile/${String(project.id)}/${envelopeId ?? 'none'}/${n}`;
140
+ await ctx.blobs.put(blob, item.payload);
141
+ await ctx.record(PROFILE, { ...base, kind: type, profile_id: p.profile_id ?? p.event_id ?? null, chunk_id: p.chunk_id ?? null, platform: p.platform ?? null, release: p.release ?? null, size: item.payload.length, blob }, id(PROFILE));
142
+ await ctx.write('project', String(project.id), { hasProfiles: true }, 'project.update');
143
+ break;
144
+ }
145
+ case 'check_in': {
146
+ const slug = String(p.monitor_slug ?? '');
147
+ const config = obj(p.monitor_config);
148
+ const monitor = ctx.rowsRaw(MONITOR).find((m) => m._project === project.id && m.slug === slug);
149
+ const monitorId = monitor ? String(monitor.id) : ctx.mint(MONITOR);
150
+ const monitorConfig = Object.keys(config).length ? {
151
+ schedule_type: obj(config.schedule).type, schedule: obj(config.schedule).value,
152
+ checkin_margin: config.checkin_margin ?? null, max_runtime: config.max_runtime ?? null, timezone: config.timezone ?? null,
153
+ failure_issue_threshold: config.failure_issue_threshold ?? null, recovery_threshold: config.recovery_threshold ?? null, alert_rule_id: null,
154
+ } : monitor?.config ?? {};
155
+ if (Object.keys(config).length) await ctx.write(MONITOR, monitorId, {
156
+ id: monitorId, _project: project.id, _organization: project._organization,
157
+ slug, name: slug, config: monitorConfig, status: 'active', isMuted: false, isUpserting: true,
158
+ dateCreated: monitor?.dateCreated ?? ctx.occurredAt, project: Object.fromEntries(Object.entries(ctx.own(project)).filter(([k]) => !k.startsWith('_'))),
159
+ environments: [], owner: null, alertRule: null,
160
+ }, monitor ? 'monitor.update' : 'monitor.create');
161
+ // source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/monitors/serializers.py "str(obj.guid)"
162
+ const rawCheckInId = String(p.check_in_id ?? ctx.mint(CHECK_IN)).replace(/-/g, '');
163
+ const checkInId = rawCheckInId.replace(/^(.{8})(.{4})(.{4})(.{4})(.{12})$/, '$1-$2-$3-$4-$5');
164
+ const prior = ctx.row(CHECK_IN, checkInId);
165
+ if (prior && p.status !== prior.status) ctx.legal(CHECK_IN, 'status', 'sendEnvelope', prior.status, String(p.status), checkInId, 'vendor');
166
+ await ctx.write(CHECK_IN, checkInId, {
167
+ id: checkInId, _monitor: monitor || Object.keys(config).length ? monitorId : null,
168
+ status: p.status ?? null, duration: p.duration ?? prior?.duration ?? null, environment: p.environment ?? '',
169
+ dateCreated: prior?.dateCreated ?? ctx.occurredAt, dateAdded: prior?.dateAdded ?? ctx.occurredAt, dateUpdated: ctx.occurredAt,
170
+ dateInProgress: prior?.dateInProgress ?? (p.status === 'in_progress' ? ctx.occurredAt : null), dateClock: ctx.occurredAt, expectedTime: prior?.expectedTime ?? ctx.occurredAt,
171
+ monitorConfig, groups: [],
172
+ }, prior ? 'check_in.update' : 'check_in.create');
173
+ await ctx.write('project', String(project.id), { hasMonitors: true }, 'project.update');
174
+ break;
175
+ }
176
+ case 'log': for (const l of Array.isArray(p.items) ? p.items as Row[] : []) await ctx.record(LOG, { ...base, timestamp: typeof l.timestamp === 'number' ? new Date(l.timestamp * 1000).toISOString() : l.timestamp ?? null, level: l.level ?? null, body: l.body ?? null, trace_id: l.trace_id ?? null, attributes: obj(l.attributes) }, id(LOG)); break;
177
+ case 'trace_metric': for (const m of Array.isArray(p.items) ? p.items as Row[] : []) await ctx.record(METRIC, { ...base, timestamp: m.timestamp ?? null, name: m.name ?? null, type: m.type ?? null, value: m.value ?? null, unit: m.unit ?? null, attributes: obj(m.attributes) }, id(METRIC)); break;
178
+ // an item of a type the twin does not keep otherwise is skipped and retained as it came
179
+ // source: https://raw.githubusercontent.com/getsentry/sentry-docs/3ec4e139711af82b38a3c2c17261f5f5cdb66dfc/develop-docs/sdk/foundations/envelopes/index.mdx "Implementations **must gracefully skip and retain** Items of unknown type"
180
+ default: {
181
+ const blob = `envelope-item/${String(project.id)}/${envelopeN}/${n}`;
182
+ await ctx.blobs.put(blob, item.payload);
183
+ await ctx.record(OTHER_ITEM, { ...base, type, header: item.header, size: item.payload.length, blob }, id(OTHER_ITEM));
184
+ }
185
+ }
186
+ }
187
+ return Response.json(envelopeId ? { id: envelopeId } : {});
188
+ }
@@ -0,0 +1,7 @@
1
+ // What Sentry's ingest does around every request (architecture, around.ts): the body is kept as its bytes, which an
2
+ // envelope is (a compressed one is not text), before the dispatch reads it. Its CORS is the manifest's (`cors`).
3
+ import { s3, type HandlerContext } from '@volter/world-core';
4
+
5
+ export async function around(ctx: HandlerContext, next: (request?: Request) => Promise<Response>): Promise<Response> {
6
+ return next(await s3.withBytes(ctx.call.request));
7
+ }
@@ -0,0 +1,8 @@
1
+ // GENERATED by scripts/create-pack.ts --index: the handler map, by operationId, from the families' files (each
2
+ // handler named as its operation). Do not edit; add a handler to its family's file and generate again.
3
+ import type { Handler } from '@volter/world-core';
4
+ import * as api from './api.ts';
5
+
6
+ export const ingestSemantics: Record<string, Handler> = {
7
+ sendEnvelope: api.sendEnvelope,
8
+ };
@@ -0,0 +1,7 @@
1
+ // What every family of ingest shares: its stored shapes, who is calling, and the vendor's answers.
2
+ import type { HandlerContext } from '@volter/world-core';
3
+
4
+ export * from '../../../src/semantics/shared.ts';
5
+ export type { HandlerContext };
6
+
7
+ export { manifest as apiManifest } from '../../../api/src/manifest.ts';
@@ -0,0 +1,2 @@
1
+ // ingest's state machines and state rulings, each transition cited (architecture, "State").
2
+ export const states = {};
@@ -0,0 +1,205 @@
1
+ {
2
+ "vendor": "sentry",
3
+ "missing": 11,
4
+ "stale": [],
5
+ "code": {
6
+ "lines": 919,
7
+ "run": 900,
8
+ "recorded": 8,
9
+ "missing": 11
10
+ },
11
+ "state": {
12
+ "declared": 2,
13
+ "reached": 2,
14
+ "recorded": 0,
15
+ "missing": [],
16
+ "undeclared": []
17
+ },
18
+ "files": [
19
+ {
20
+ "file": "semantics/shared.ts",
21
+ "lines": {
22
+ "found": 279,
23
+ "hit": 277
24
+ },
25
+ "missing": []
26
+ },
27
+ {
28
+ "file": "manifest.ts",
29
+ "lines": {
30
+ "found": 33,
31
+ "hit": 33
32
+ },
33
+ "missing": []
34
+ },
35
+ {
36
+ "file": "index.ts",
37
+ "lines": {
38
+ "found": 10,
39
+ "hit": 10
40
+ },
41
+ "missing": []
42
+ },
43
+ {
44
+ "file": "server.ts",
45
+ "lines": {
46
+ "found": 4,
47
+ "hit": 2
48
+ },
49
+ "missing": []
50
+ },
51
+ {
52
+ "file": "fetch.ts",
53
+ "lines": {
54
+ "found": 6,
55
+ "hit": 6
56
+ },
57
+ "missing": []
58
+ },
59
+ {
60
+ "file": "../api/src/semantics/doors.ts",
61
+ "lines": {
62
+ "found": 27,
63
+ "hit": 23
64
+ },
65
+ "missing": []
66
+ },
67
+ {
68
+ "file": "../api/src/semantics/states.ts",
69
+ "lines": {
70
+ "found": 9,
71
+ "hit": 9
72
+ },
73
+ "missing": []
74
+ },
75
+ {
76
+ "file": "../api/src/semantics/api.ts",
77
+ "lines": {
78
+ "found": 164,
79
+ "hit": 157
80
+ },
81
+ "missing": [
82
+ {
83
+ "from": 38,
84
+ "to": 41,
85
+ "fn": "makeRelease"
86
+ },
87
+ {
88
+ "from": 222,
89
+ "to": 224,
90
+ "fn": "listOrganizationReleases"
91
+ }
92
+ ]
93
+ },
94
+ {
95
+ "file": "../api/src/semantics/shared.ts",
96
+ "lines": {
97
+ "found": 17,
98
+ "hit": 17
99
+ },
100
+ "missing": []
101
+ },
102
+ {
103
+ "file": "../api/src/semantics/around.ts",
104
+ "lines": {
105
+ "found": 27,
106
+ "hit": 23
107
+ },
108
+ "missing": [
109
+ {
110
+ "from": 27,
111
+ "to": 30,
112
+ "fn": "around"
113
+ }
114
+ ]
115
+ },
116
+ {
117
+ "file": "../api/src/semantics/index.ts",
118
+ "lines": {
119
+ "found": 15,
120
+ "hit": 15
121
+ },
122
+ "missing": []
123
+ },
124
+ {
125
+ "file": "../api/src/manifest.ts",
126
+ "lines": {
127
+ "found": 112,
128
+ "hit": 112
129
+ },
130
+ "missing": []
131
+ },
132
+ {
133
+ "file": "../api/src/index.ts",
134
+ "lines": {
135
+ "found": 2,
136
+ "hit": 2
137
+ },
138
+ "missing": []
139
+ },
140
+ {
141
+ "file": "../api/src/fetch.ts",
142
+ "lines": {
143
+ "found": 14,
144
+ "hit": 14
145
+ },
146
+ "missing": []
147
+ },
148
+ {
149
+ "file": "../ingest/src/semantics/api.ts",
150
+ "lines": {
151
+ "found": 153,
152
+ "hit": 153
153
+ },
154
+ "missing": []
155
+ },
156
+ {
157
+ "file": "../ingest/src/semantics/shared.ts",
158
+ "lines": {
159
+ "found": 2,
160
+ "hit": 2
161
+ },
162
+ "missing": []
163
+ },
164
+ {
165
+ "file": "../ingest/src/semantics/around.ts",
166
+ "lines": {
167
+ "found": 3,
168
+ "hit": 3
169
+ },
170
+ "missing": []
171
+ },
172
+ {
173
+ "file": "../ingest/src/semantics/index.ts",
174
+ "lines": {
175
+ "found": 4,
176
+ "hit": 4
177
+ },
178
+ "missing": []
179
+ },
180
+ {
181
+ "file": "../ingest/src/manifest.ts",
182
+ "lines": {
183
+ "found": 24,
184
+ "hit": 24
185
+ },
186
+ "missing": []
187
+ },
188
+ {
189
+ "file": "../ingest/src/index.ts",
190
+ "lines": {
191
+ "found": 2,
192
+ "hit": 2
193
+ },
194
+ "missing": []
195
+ },
196
+ {
197
+ "file": "../ingest/src/fetch.ts",
198
+ "lines": {
199
+ "found": 12,
200
+ "hit": 12
201
+ },
202
+ "missing": []
203
+ }
204
+ ]
205
+ }