@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.
Files changed (189) hide show
  1. package/README.md +28 -140
  2. package/api/spec/SOURCE.md +32 -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 +137 -0
  12. package/api/src/semantics/api.ts +247 -0
  13. package/api/src/semantics/around.ts +36 -0
  14. package/api/src/semantics/doors.ts +76 -0
  15. package/api/src/semantics/index.ts +19 -0
  16. package/api/src/semantics/shared.ts +31 -0
  17. package/api/src/semantics/states.ts +17 -0
  18. package/dist/api/spec/SOURCE.md +32 -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 +136 -0
  33. package/dist/api/src/manifest.ts +137 -0
  34. package/dist/api/src/semantics/api.d.ts +13 -0
  35. package/dist/api/src/semantics/api.js +269 -0
  36. package/dist/api/src/semantics/api.ts +247 -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 +5 -0
  41. package/dist/api/src/semantics/doors.js +71 -0
  42. package/dist/api/src/semantics/doors.ts +76 -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 +14 -0
  47. package/dist/api/src/semantics/shared.js +26 -0
  48. package/dist/api/src/semantics/shared.ts +31 -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/ingest/spec/SOURCE.md +30 -0
  53. package/dist/ingest/spec/authentication.mdx.gz +0 -0
  54. package/dist/ingest/spec/compression.mdx.gz +0 -0
  55. package/dist/ingest/spec/doc-examples.json +216 -0
  56. package/dist/ingest/spec/envelope-items.mdx.gz +0 -0
  57. package/dist/ingest/spec/envelopes.mdx.gz +0 -0
  58. package/dist/ingest/spec/openapi.json +178 -0
  59. package/dist/ingest/spec/rate-limiting.mdx.gz +0 -0
  60. package/dist/ingest/spec/sources.json +25 -0
  61. package/dist/ingest/spec/transport.mdx.gz +0 -0
  62. package/dist/ingest/spec/write-spec.ts +192 -0
  63. package/dist/ingest/src/fetch.d.ts +2 -0
  64. package/dist/ingest/src/fetch.js +15 -0
  65. package/dist/ingest/src/fetch.ts +16 -0
  66. package/dist/ingest/src/generated/surface.gen.json +1 -0
  67. package/dist/ingest/src/generated/ui.gen.json +1 -0
  68. package/dist/ingest/src/index.d.ts +2 -0
  69. package/dist/ingest/src/index.js +3 -0
  70. package/dist/ingest/src/index.ts +3 -0
  71. package/dist/ingest/src/manifest.d.ts +2 -0
  72. package/dist/ingest/src/manifest.js +37 -0
  73. package/dist/ingest/src/manifest.ts +42 -0
  74. package/dist/ingest/src/semantics/api.d.ts +2 -0
  75. package/dist/ingest/src/semantics/api.js +240 -0
  76. package/dist/ingest/src/semantics/api.ts +188 -0
  77. package/dist/ingest/src/semantics/around.d.ts +2 -0
  78. package/dist/ingest/src/semantics/around.js +6 -0
  79. package/dist/ingest/src/semantics/around.ts +7 -0
  80. package/dist/ingest/src/semantics/index.d.ts +2 -0
  81. package/dist/ingest/src/semantics/index.js +4 -0
  82. package/dist/ingest/src/semantics/index.ts +8 -0
  83. package/dist/ingest/src/semantics/shared.d.ts +4 -0
  84. package/dist/ingest/src/semantics/shared.js +2 -0
  85. package/dist/ingest/src/semantics/shared.ts +7 -0
  86. package/dist/ingest/src/semantics/states.d.ts +1 -0
  87. package/dist/ingest/src/semantics/states.js +2 -0
  88. package/dist/ingest/src/semantics/states.ts +2 -0
  89. package/dist/journeys/coverage.json +205 -0
  90. package/dist/journeys/customer-life.json +921 -0
  91. package/dist/journeys/customer-life.outline.md +51 -0
  92. package/dist/journeys/decisions.json +170 -0
  93. package/dist/journeys/demand.json +92 -0
  94. package/dist/journeys/first-use.json +22 -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 +13 -75
  104. package/dist/src/manifest.d.ts +2 -0
  105. package/dist/src/manifest.js +35 -0
  106. package/dist/src/semantics/shared.d.ts +88 -0
  107. package/dist/src/semantics/shared.js +407 -0
  108. package/dist/src/server.d.ts +7 -0
  109. package/dist/src/server.js +7 -0
  110. package/generated/pack-facts.json +101 -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 +42 -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 +921 -0
  134. package/journeys/customer-life.outline.md +51 -0
  135. package/journeys/decisions.json +170 -0
  136. package/journeys/demand.json +92 -0
  137. package/journeys/first-use.json +22 -0
  138. package/journeys/score.json +231 -0
  139. package/journeys/unreachable.json +29 -0
  140. package/journeys/vendor-examples.json +474 -0
  141. package/package.json +22 -47
  142. package/spec/sources.json +76 -0
  143. package/src/cli.ts +12 -24
  144. package/src/fetch.ts +10 -0
  145. package/src/index.ts +13 -143
  146. package/src/manifest.ts +41 -0
  147. package/src/semantics/shared.ts +439 -0
  148. package/src/server.ts +8 -0
  149. package/client/sentry-mirror.css +0 -99
  150. package/client/sentry-mirror.tsx +0 -352
  151. package/dist/client/sentry-mirror.bundle.js +0 -388
  152. package/dist/client/sentry-mirror.css +0 -99
  153. package/dist/client/sentry-mirror.d.ts +0 -17
  154. package/dist/client/sentry-mirror.js +0 -156
  155. package/dist/client/sentry-mirror.tsx +0 -352
  156. package/dist/src/sentry-budget.d.ts +0 -50
  157. package/dist/src/sentry-budget.js +0 -145
  158. package/dist/src/sentry-capabilities.d.ts +0 -3
  159. package/dist/src/sentry-capabilities.js +0 -1180
  160. package/dist/src/sentry-conformance.d.ts +0 -22
  161. package/dist/src/sentry-conformance.js +0 -97
  162. package/dist/src/sentry-connector.d.ts +0 -77
  163. package/dist/src/sentry-connector.js +0 -226
  164. package/dist/src/sentry-events.d.ts +0 -54
  165. package/dist/src/sentry-events.js +0 -132
  166. package/dist/src/sentry-ingest.d.ts +0 -137
  167. package/dist/src/sentry-ingest.js +0 -387
  168. package/dist/src/sentry-mirror-ui.d.ts +0 -38
  169. package/dist/src/sentry-mirror-ui.js +0 -162
  170. package/dist/src/sentry-perform-harness.d.ts +0 -9
  171. package/dist/src/sentry-perform-harness.js +0 -20
  172. package/dist/src/sentry-server.d.ts +0 -14
  173. package/dist/src/sentry-server.js +0 -27
  174. package/dist/src/sentry-twin.d.ts +0 -17
  175. package/dist/src/sentry-twin.js +0 -1739
  176. package/dist/test-fixtures/sentry-openapi-operations.SOURCE.md +0 -24
  177. package/dist/test-fixtures/sentry-openapi-operations.json +0 -1837
  178. package/src/sentry-budget.ts +0 -171
  179. package/src/sentry-capabilities.ts +0 -1222
  180. package/src/sentry-conformance.ts +0 -110
  181. package/src/sentry-connector.ts +0 -260
  182. package/src/sentry-events.ts +0 -172
  183. package/src/sentry-ingest.ts +0 -471
  184. package/src/sentry-mirror-ui.ts +0 -162
  185. package/src/sentry-perform-harness.ts +0 -19
  186. package/src/sentry-server.ts +0 -35
  187. package/src/sentry-twin.ts +0 -1615
  188. package/test-fixtures/sentry-openapi-operations.SOURCE.md +0 -24
  189. package/test-fixtures/sentry-openapi-operations.json +0 -1837
@@ -0,0 +1,192 @@
1
+ // Offline envelope specification: the pinned reference bytes named in SOURCE.md own the facts.
2
+ import { readFileSync, writeFileSync } from 'node:fs';
3
+ import { gunzipSync } from 'node:zlib';
4
+ const reference = (name: string) => gunzipSync(readFileSync(new URL(name + '.mdx.gz', import.meta.url))).toString();
5
+ const envelopes = reference('envelopes');
6
+ const transport = reference('transport');
7
+ const authentication = reference('authentication');
8
+ const items = reference('envelope-items');
9
+ const compression = reference('compression');
10
+ reference('rate-limiting');
11
+ for (const [text, words] of [[envelopes, 'POST /api/<project_id>/envelope/'], [envelopes, '403 Forbidden'], [envelopes, '200 MiB'], [authentication, 'sentry_key'], [items, 'check_in'], [transport, '200'], [compression, 'zstd']]) {
12
+ if (!text.includes(words)) throw new Error('Pinned reference does not hold: ' + words);
13
+ }
14
+ const document = {
15
+ "openapi": "3.0.3",
16
+ "info": {
17
+ "title": "Sentry ingest (Relay): the envelope endpoint",
18
+ "version": "sentry-docs 3ec4e139",
19
+ "description": "Written from Sentry's SDK developer documentation (spec/SOURCE.md): the one endpoint every Sentry SDK sends its data to. Sentry publishes no OpenAPI document for it."
20
+ },
21
+ "servers": [
22
+ {
23
+ "url": "https://o0.ingest.us.sentry.io",
24
+ "description": "the DSN's host: o<org id>.ingest[.<region>].sentry.io"
25
+ }
26
+ ],
27
+ "paths": {
28
+ "/api/{project_id}/envelope/": {
29
+ "post": {
30
+ "operationId": "sendEnvelope",
31
+ "summary": "Ingest an envelope",
32
+ "description": "The main ingestion endpoint for Envelopes. An envelope is a header line and items, each an item header line and its payload (an event, a transaction, a session, an attachment, a replay, a profile, a check-in, feedback, logs, a client report).",
33
+ "tags": [
34
+ "Ingest"
35
+ ],
36
+ "parameters": [
37
+ {
38
+ "name": "project_id",
39
+ "in": "path",
40
+ "required": true,
41
+ "schema": {
42
+ "type": "string",
43
+ "pattern": "^[0-9]+$"
44
+ },
45
+ "description": "The DSN's project id"
46
+ },
47
+ {
48
+ "name": "sentry_key",
49
+ "in": "query",
50
+ "required": false,
51
+ "schema": {
52
+ "type": "string"
53
+ },
54
+ "description": "The DSN's public key, where the X-Sentry-Auth header cannot be sent"
55
+ },
56
+ {
57
+ "name": "sentry_version",
58
+ "in": "query",
59
+ "required": false,
60
+ "schema": {
61
+ "type": "string"
62
+ }
63
+ },
64
+ {
65
+ "name": "sentry_client",
66
+ "in": "query",
67
+ "required": false,
68
+ "schema": {
69
+ "type": "string"
70
+ }
71
+ },
72
+ {
73
+ "name": "X-Sentry-Auth",
74
+ "in": "header",
75
+ "required": false,
76
+ "schema": {
77
+ "type": "string"
78
+ },
79
+ "description": "Sentry sentry_version=7, sentry_client=<name/version>, sentry_key=<public key>"
80
+ },
81
+ {
82
+ "name": "Content-Encoding",
83
+ "in": "header",
84
+ "required": false,
85
+ "schema": {
86
+ "type": "string",
87
+ "enum": [
88
+ "gzip",
89
+ "deflate",
90
+ "br",
91
+ "zstd"
92
+ ]
93
+ }
94
+ }
95
+ ],
96
+ "requestBody": {
97
+ "required": true,
98
+ "content": {
99
+ "application/x-sentry-envelope": {
100
+ "schema": {
101
+ "type": "string",
102
+ "format": "binary"
103
+ }
104
+ },
105
+ "text/plain": {
106
+ "schema": {
107
+ "type": "string",
108
+ "format": "binary"
109
+ }
110
+ }
111
+ }
112
+ },
113
+ "responses": {
114
+ "200": {
115
+ "description": "The envelope was accepted",
116
+ "content": {
117
+ "application/json": {
118
+ "schema": {
119
+ "type": "object",
120
+ "properties": {
121
+ "id": {
122
+ "type": "string",
123
+ "description": "the envelope's event_id, when it has one"
124
+ }
125
+ }
126
+ },
127
+ "example": {
128
+ "id": "fc6d8c0c43fc4630ad850ee518f1b9d0"
129
+ }
130
+ }
131
+ }
132
+ },
133
+ "400": {
134
+ "description": "The envelope could not be read",
135
+ "headers": {
136
+ "X-Sentry-Error": {
137
+ "schema": {
138
+ "type": "string"
139
+ }
140
+ }
141
+ },
142
+ "content": {
143
+ "application/json": {
144
+ "schema": {
145
+ "type": "object",
146
+ "properties": {
147
+ "detail": {
148
+ "type": "string"
149
+ },
150
+ "causes": {
151
+ "type": "array",
152
+ "items": {
153
+ "type": "string"
154
+ }
155
+ }
156
+ }
157
+ }
158
+ }
159
+ }
160
+ },
161
+ "403": {
162
+ "description": "No authentication, or a key the project does not have"
163
+ },
164
+ "413": {
165
+ "description": "The envelope or an item exceeds its size limit"
166
+ },
167
+ "429": {
168
+ "description": "Rate limited",
169
+ "headers": {
170
+ "Retry-After": {
171
+ "schema": {
172
+ "type": "string"
173
+ }
174
+ },
175
+ "X-Sentry-Rate-Limits": {
176
+ "schema": {
177
+ "type": "string"
178
+ }
179
+ }
180
+ }
181
+ }
182
+ }
183
+ }
184
+ }
185
+ },
186
+ "tags": [
187
+ {
188
+ "name": "Ingest"
189
+ }
190
+ ]
191
+ };
192
+ writeFileSync(new URL('openapi.json', import.meta.url), JSON.stringify(document, null, 2) + '\n');
@@ -0,0 +1,2 @@
1
+ import { type DerivedFetch, type PackFetchOptions } from '@volter/world-core';
2
+ export declare function createIngestLaneFetch(options?: PackFetchOptions): DerivedFetch;
@@ -0,0 +1,15 @@
1
+ // GENERATED by scripts/create-pack.ts (--init, --index): a fixed file. The kernel serves the discovery door, the clock,
2
+ // the World's doors, the vendor's screens and the API from the pack's parts (architecture, "The pack contract").
3
+ import { createPackFetch } from '@volter/world-core';
4
+ import surface from './generated/surface.gen.json' with { type: 'json' };
5
+ import { manifest } from "./manifest.js";
6
+ import { around } from "./semantics/around.js";
7
+ import { ingestSemantics } from "./semantics/index.js";
8
+ export function createIngestLaneFetch(options = {}) {
9
+ return createPackFetch({
10
+ manifest,
11
+ surface: surface,
12
+ handlers: ingestSemantics,
13
+ around,
14
+ }, options);
15
+ }
@@ -0,0 +1,16 @@
1
+ // GENERATED by scripts/create-pack.ts (--init, --index): a fixed file. The kernel serves the discovery door, the clock,
2
+ // the World's doors, the vendor's screens and the API from the pack's parts (architecture, "The pack contract").
3
+ import { createPackFetch, type DerivedFetch, type DerivedSurface, type PackFetchOptions } from '@volter/world-core';
4
+ import surface from './generated/surface.gen.json' with { type: 'json' };
5
+ import { manifest } from './manifest.ts';
6
+ import { around } from './semantics/around.ts';
7
+ import { ingestSemantics } from './semantics/index.ts';
8
+
9
+ export function createIngestLaneFetch(options: PackFetchOptions = {}): DerivedFetch {
10
+ return createPackFetch({
11
+ manifest,
12
+ surface: surface as DerivedSurface,
13
+ handlers: ingestSemantics,
14
+ around,
15
+ }, options);
16
+ }
@@ -0,0 +1 @@
1
+ {"generatedBy":"scripts/derive-pack.ts","spec":"openapi.json","format":"openapi","version":"sentry-docs 3ec4e139","resources":[],"operations":[{"id":"sendEnvelope","method":"post","path":"/api/{project_id}/envelope/","pathParams":["project_id"],"query":[{"name":"sentry_client","type":"string","required":false},{"name":"sentry_key","type":"string","required":false},{"name":"sentry_version","type":"string","required":false}],"body":[{"name":"body","type":"blob","required":true}],"bodyEncoding":"none","successStatus":200,"class":"non-resource"}]}
@@ -0,0 +1 @@
1
+ {"generatedBy":"scripts/derive-pack.ts","resources":{}}
@@ -0,0 +1,2 @@
1
+ export { createIngestLaneFetch } from './fetch.js';
2
+ export { manifest } from './manifest.js';
@@ -0,0 +1,3 @@
1
+ // The ingest lane of the sentry pack: the vendor's manifest routes its requests here.
2
+ export { createIngestLaneFetch } from "./fetch.js";
3
+ export { manifest } from "./manifest.js";
@@ -0,0 +1,3 @@
1
+ // The ingest lane of the sentry pack: the vendor's manifest routes its requests here.
2
+ export { createIngestLaneFetch } from './fetch.ts';
3
+ export { manifest } from './manifest.ts';
@@ -0,0 +1,2 @@
1
+ import type { DerivedManifest } from '@volter/world-core';
2
+ export declare const manifest: DerivedManifest;
@@ -0,0 +1,37 @@
1
+ export const manifest = {
2
+ vendor: 'sentry',
3
+ // one service with the API lane (../../api/src/manifest.ts), so one store
4
+ service: 'sentry',
5
+ hosts: [{ suffix: '.ingest.sentry.io' }, { suffix: '.ingest.us.sentry.io' }, { suffix: '.ingest.de.sentry.io' }],
6
+ // source: spec:/servers/0/url "https://o0.ingest.us.sentry.io"
7
+ remoteHost: { template: 'o0.ingest.us.sentry.io' },
8
+ // an envelope is not JSON: its bytes are kept (./semantics/around.ts) and read by the handler
9
+ body: {},
10
+ // 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:"
11
+ // The kernel decodes the declared vendor encodings before the handler reads the retained bytes.
12
+ encodings: { request: ['gzip', 'deflate', 'br', 'zstd'], undecodable: { status: 400, message: 'failed to read request body' } },
13
+ ids: { template: '{uuid}' },
14
+ time: 'iso',
15
+ // Relay's error body: `{"detail": ..., "causes": [...]}` (develop-docs/sdk/foundations/transport/index.mdx)
16
+ error: { detail: '{message}' },
17
+ readOnly: { status: 403, message: 'This twin was started read-only; envelopes are refused.' },
18
+ malformedBody: { status: 400, message: 'failed to read request body' },
19
+ notFound: { status: 404, message: 'Not Found' },
20
+ gap: { status: 404, message: 'Not Found' },
21
+ deleted: {},
22
+ // a browser SDK posts cross-origin: every answer allows any origin and exposes what an SDK reads of it
23
+ // (transport/rate-limiting.mdx: Retry-After, X-Sentry-Rate-Limits). Where the documentation stops: Relay's CORS is its
24
+ // code's, not a page's; the headers allowed are the ones the SDKs' transports send
25
+ cors: {
26
+ origin: '*',
27
+ methods: 'GET, POST, OPTIONS',
28
+ headers: 'x-sentry-auth, x-requested-with, x-forwarded-for, origin, referer, accept, content-type, authentication, authorization, content-encoding, transfer-encoding, sentry-trace, baggage',
29
+ expose: 'x-sentry-error, x-sentry-rate-limits, retry-after',
30
+ maxAge: 3600,
31
+ },
32
+ // the sequences the vendor numbers by (ctx.issue over each, src/semantics/shared.ts next)
33
+ // All stored vendor resources are owned and refreshed by the API manifest through ctx.over.
34
+ resources: {},
35
+ vendorBacked: { none: 'Envelope receipt is telemetry ingestion, never deployed as account changes. The API manifest owns and refreshes its resulting vendor resources.' },
36
+ 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' },
37
+ };
@@ -0,0 +1,42 @@
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
+ hosts: [{ suffix: '.ingest.sentry.io' }, { suffix: '.ingest.us.sentry.io' }, { suffix: '.ingest.de.sentry.io' }],
11
+ // source: spec:/servers/0/url "https://o0.ingest.us.sentry.io"
12
+ remoteHost: { template: 'o0.ingest.us.sentry.io' },
13
+ // an envelope is not JSON: its bytes are kept (./semantics/around.ts) and read by the handler
14
+ body: {},
15
+ // 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:"
16
+ // The kernel decodes the declared vendor encodings before the handler reads the retained bytes.
17
+ encodings: { request: ['gzip', 'deflate', 'br', 'zstd'], undecodable: { status: 400, message: 'failed to read request body' } },
18
+ ids: { template: '{uuid}' },
19
+ time: 'iso',
20
+ // Relay's error body: `{"detail": ..., "causes": [...]}` (develop-docs/sdk/foundations/transport/index.mdx)
21
+ error: { detail: '{message}' },
22
+ readOnly: { status: 403, message: 'This twin was started read-only; envelopes are refused.' },
23
+ malformedBody: { status: 400, message: 'failed to read request body' },
24
+ notFound: { status: 404, message: 'Not Found' },
25
+ gap: { status: 404, message: 'Not Found' },
26
+ deleted: {},
27
+ // a browser SDK posts cross-origin: every answer allows any origin and exposes what an SDK reads of it
28
+ // (transport/rate-limiting.mdx: Retry-After, X-Sentry-Rate-Limits). Where the documentation stops: Relay's CORS is its
29
+ // code's, not a page's; the headers allowed are the ones the SDKs' transports send
30
+ cors: {
31
+ origin: '*',
32
+ methods: 'GET, POST, OPTIONS',
33
+ headers: 'x-sentry-auth, x-requested-with, x-forwarded-for, origin, referer, accept, content-type, authentication, authorization, content-encoding, transfer-encoding, sentry-trace, baggage',
34
+ expose: 'x-sentry-error, x-sentry-rate-limits, retry-after',
35
+ maxAge: 3600,
36
+ },
37
+ // the sequences the vendor numbers by (ctx.issue over each, src/semantics/shared.ts next)
38
+ // All stored vendor resources are owned and refreshed by the API manifest through ctx.over.
39
+ resources: {},
40
+ vendorBacked: { none: 'Envelope receipt is telemetry ingestion, never deployed as account changes. The API manifest owns and refreshes its resulting vendor resources.' },
41
+ 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' },
42
+ };
@@ -0,0 +1,2 @@
1
+ import { type HandlerContext } from '@volter/world-core';
2
+ export declare function sendEnvelope(ctx: HandlerContext): Promise<Response>;
@@ -0,0 +1,240 @@
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 } from '@volter/world-core';
5
+ import { apiManifest, ATTACHMENT, CHECK_IN, CLIENT_REPORT, keepEvent, LOG, METRIC, MONITOR, next, OTHER_ITEM, PROFILE, projectByKey, REPLAY, SESSION } from "./shared.js";
6
+ const obj = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : {});
7
+ const refuse = (status, detail) => Response.json({ detail }, { status, headers: { 'x-sentry-error': detail } });
8
+ // 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."
9
+ const MAX_ENVELOPE = 200 * 1024 * 1024;
10
+ /** The body's bytes: the kernel has decoded a gzip, deflate, br or zstd body (the manifest's `encodings`) and dropped the
11
+ * header, so a Content-Encoding still on the request is one Sentry does not read. */
12
+ function decoded(bytes, encoding) {
13
+ const coding = (encoding ?? '').toLowerCase();
14
+ return coding === '' || coding === 'identity' ? bytes : `unsupported content encoding: ${encoding}`;
15
+ }
16
+ /** An envelope read: its header and items. An item's header may give its `length`, else it runs to the newline
17
+ * (develop-docs/sdk/foundations/envelopes/index.mdx, "Envelopes" and "Items"). */
18
+ function parse(bytes) {
19
+ let at = 0;
20
+ const line = () => {
21
+ if (at >= bytes.length)
22
+ return undefined;
23
+ let end = bytes.indexOf(10, at);
24
+ if (end < 0)
25
+ end = bytes.length;
26
+ const out = bytes.subarray(at, end);
27
+ at = end + 1;
28
+ return out;
29
+ };
30
+ const json = (b) => {
31
+ if (!b)
32
+ return undefined;
33
+ try {
34
+ const v = JSON.parse(new TextDecoder().decode(b));
35
+ return v && typeof v === 'object' && !Array.isArray(v) ? v : undefined;
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ };
41
+ const header = json(line());
42
+ if (!header)
43
+ return 'invalid envelope header';
44
+ const items = [];
45
+ while (at < bytes.length) {
46
+ const raw = line();
47
+ if (!raw || raw.length === 0 || (raw.length === 1 && raw[0] === 13))
48
+ continue;
49
+ const h = json(raw);
50
+ if (!h || typeof h.type !== 'string')
51
+ return 'invalid item header';
52
+ let payload;
53
+ if (typeof h.length === 'number') {
54
+ if (!Number.isInteger(h.length) || h.length < 0)
55
+ return 'invalid item length';
56
+ if (at + h.length > bytes.length)
57
+ return 'item payload is shorter than its length';
58
+ payload = bytes.subarray(at, at + h.length);
59
+ at += h.length;
60
+ if (at < bytes.length && bytes[at] !== 10)
61
+ return 'length-prefixed payload must end with newline or EOF';
62
+ if (bytes[at] === 10)
63
+ at += 1;
64
+ }
65
+ else
66
+ payload = line() ?? new Uint8Array(0);
67
+ items.push({ header: h, payload });
68
+ }
69
+ return { header, items };
70
+ }
71
+ /** The public key a request authenticates with: X-Sentry-Auth's `sentry_key`, the query's, or the envelope's DSN. */
72
+ function publicKeyOf(request, header) {
73
+ const auth = request.headers.get('x-sentry-auth') ?? '';
74
+ const fromHeader = /sentry_key=([^,\s]+)/.exec(auth)?.[1];
75
+ const fromQuery = new URL(request.url).searchParams.get('sentry_key') ?? undefined;
76
+ let fromDsn;
77
+ if (typeof header.dsn === 'string') {
78
+ try {
79
+ fromDsn = new URL(header.dsn).username || undefined;
80
+ }
81
+ catch { /* not a URL */ }
82
+ }
83
+ const given = [fromHeader, fromQuery, fromDsn].filter((k) => !!k);
84
+ // "If multiple forms of authentication are given, the endpoint validates that the information matches and otherwise
85
+ // rejects the request" (envelopes/index.mdx, Authentication)
86
+ return given.length && given.every((k) => k === given[0]) ? given[0] : given.length ? '' : undefined;
87
+ }
88
+ const text = (b) => new TextDecoder().decode(b);
89
+ const asJson = (b) => { try {
90
+ return obj(JSON.parse(text(b)));
91
+ }
92
+ catch {
93
+ return {};
94
+ } };
95
+ // source: spec:sendEnvelope "The main ingestion endpoint for Envelopes."
96
+ export async function sendEnvelope(ctx) {
97
+ const request = ctx.call.request;
98
+ const body = decoded(s3.bytesOf(request), request.headers.get('content-encoding'));
99
+ ctx = await ctx.over(apiManifest);
100
+ if (typeof body === 'string')
101
+ return refuse(400, body);
102
+ if (body.length > MAX_ENVELOPE)
103
+ return refuse(413, 'envelope exceeded size limits');
104
+ const envelope = parse(body);
105
+ if (typeof envelope === 'string')
106
+ return refuse(400, `invalid envelope: ${envelope}`);
107
+ const key = publicKeyOf(request, envelope.header);
108
+ // "If both are missing, the Envelope is rejected with status code `403 Forbidden`." (envelopes/index.mdx)
109
+ if (key === undefined)
110
+ return refuse(403, 'missing authorization information');
111
+ if (key === '')
112
+ return refuse(403, 'the authentication given does not match');
113
+ // Where the documentation stops: Relay's answer for a key no project has is not a page's; the twin refuses it as
114
+ // missing authentication is refused, and a key of another project than the path's likewise
115
+ const project = projectByKey(ctx, key);
116
+ if (!project || String(project.id) !== String(ctx.call.params.project_id))
117
+ return refuse(403, 'invalid api key');
118
+ const org = obj(ctx.row('organization', String(project._organization)));
119
+ const envelopeId = typeof envelope.header.event_id === 'string' ? envelope.header.event_id.replace(/-/g, '') : undefined;
120
+ const sentAt = typeof envelope.header.sent_at === 'string' ? envelope.header.sent_at : ctx.occurredAt;
121
+ const base = { project: project.id, organization: org.id ?? project._organization, received: ctx.occurredAt, sent_at: sentAt };
122
+ // 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."
123
+ let sessions = 0;
124
+ for (const item of envelope.items) {
125
+ const kind = String(item.header.type);
126
+ 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 }[kind];
127
+ if (limit && item.payload.length > limit)
128
+ return refuse(413, 'item exceeded size limits');
129
+ const payload = asJson(item.payload);
130
+ if (kind === 'session')
131
+ sessions++;
132
+ if (sessions > 100 || (kind === 'sessions' && Array.isArray(payload.aggregates) && payload.aggregates.length > 100))
133
+ return refuse(413, 'too many sessions');
134
+ if (kind === 'trace_metric' && Array.isArray(payload.items) && payload.items.some((metric) => new TextEncoder().encode(JSON.stringify(metric)).length > 2048))
135
+ return refuse(413, 'metric exceeded size limits');
136
+ }
137
+ let n = 0;
138
+ const envelopeN = await next(ctx, 'envelope');
139
+ const receipt = `envelope/${String(project.id)}/${envelopeN}`;
140
+ await ctx.blobs.put(receipt, body);
141
+ await ctx.record('_envelope', { ...base, header: envelope.header, blob: receipt, size: body.length }, `${String(project.id)}:${envelopeN}`);
142
+ const id = (kind) => `${String(project.id)}:${kind}:${envelopeN}:${n++}`;
143
+ for (const item of envelope.items) {
144
+ const type = String(item.header.type);
145
+ const p = asJson(item.payload);
146
+ switch (type) {
147
+ case 'event':
148
+ await keepEvent(ctx, project, { ...p, event_id: envelopeId ?? p.event_id }, p.exception ? 'error' : 'default');
149
+ break;
150
+ case 'transaction':
151
+ await keepEvent(ctx, project, { ...p, event_id: envelopeId ?? p.event_id }, 'transaction');
152
+ break;
153
+ case 'feedback':
154
+ await keepEvent(ctx, project, { ...p, event_id: envelopeId ?? p.event_id }, 'feedback');
155
+ break;
156
+ case 'attachment': {
157
+ const blob = `attachment/${String(project.id)}/${envelopeId ?? 'none'}/${String(item.header.filename ?? n)}`;
158
+ await ctx.blobs.put(blob, item.payload);
159
+ 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));
160
+ break;
161
+ }
162
+ case 'session':
163
+ 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))}`);
164
+ break;
165
+ case 'sessions':
166
+ await ctx.record(SESSION, { ...base, aggregates: p.aggregates ?? [], attrs: obj(p.attrs) }, id(SESSION));
167
+ break;
168
+ case 'client_report':
169
+ await ctx.record(CLIENT_REPORT, { ...base, timestamp: p.timestamp ?? null, discarded_events: p.discarded_events ?? [] }, id(CLIENT_REPORT));
170
+ break;
171
+ case 'replay_event':
172
+ 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));
173
+ break;
174
+ case 'replay_recording':
175
+ case 'replay_video': {
176
+ const blob = `replay/${String(project.id)}/${envelopeId ?? 'none'}/${n}`;
177
+ await ctx.blobs.put(blob, item.payload);
178
+ await ctx.record(REPLAY, { ...base, replay_id: envelopeId ?? null, recording: blob, kind: type, size: item.payload.length }, id(REPLAY));
179
+ break;
180
+ }
181
+ case 'profile':
182
+ case 'profile_chunk': {
183
+ const blob = `profile/${String(project.id)}/${envelopeId ?? 'none'}/${n}`;
184
+ await ctx.blobs.put(blob, item.payload);
185
+ 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));
186
+ await ctx.write('project', String(project.id), { hasProfiles: true }, 'project.update');
187
+ break;
188
+ }
189
+ case 'check_in': {
190
+ const slug = String(p.monitor_slug ?? '');
191
+ const config = obj(p.monitor_config);
192
+ const monitor = ctx.rowsRaw(MONITOR).find((m) => m._project === project.id && m.slug === slug);
193
+ const monitorId = monitor ? String(monitor.id) : ctx.mint(MONITOR);
194
+ const monitorConfig = Object.keys(config).length ? {
195
+ schedule_type: obj(config.schedule).type, schedule: obj(config.schedule).value,
196
+ checkin_margin: config.checkin_margin ?? null, max_runtime: config.max_runtime ?? null, timezone: config.timezone ?? null,
197
+ failure_issue_threshold: config.failure_issue_threshold ?? null, recovery_threshold: config.recovery_threshold ?? null, alert_rule_id: null,
198
+ } : monitor?.config ?? {};
199
+ if (Object.keys(config).length)
200
+ await ctx.write(MONITOR, monitorId, {
201
+ id: monitorId, _project: project.id, _organization: project._organization,
202
+ slug, name: slug, config: monitorConfig, status: 'active', isMuted: false, isUpserting: true,
203
+ dateCreated: monitor?.dateCreated ?? ctx.occurredAt, project: Object.fromEntries(Object.entries(ctx.own(project)).filter(([k]) => !k.startsWith('_'))),
204
+ environments: [], owner: null, alertRule: null,
205
+ }, monitor ? 'monitor.update' : 'monitor.create');
206
+ // source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/monitors/serializers.py "str(obj.guid)"
207
+ const rawCheckInId = String(p.check_in_id ?? ctx.mint(CHECK_IN)).replace(/-/g, '');
208
+ const checkInId = rawCheckInId.replace(/^(.{8})(.{4})(.{4})(.{4})(.{12})$/, '$1-$2-$3-$4-$5');
209
+ const prior = ctx.row(CHECK_IN, checkInId);
210
+ if (prior && p.status !== prior.status)
211
+ ctx.legal(CHECK_IN, 'status', 'sendEnvelope', prior.status, String(p.status), checkInId, 'vendor');
212
+ await ctx.write(CHECK_IN, checkInId, {
213
+ id: checkInId, _monitor: monitor || Object.keys(config).length ? monitorId : null,
214
+ status: p.status ?? null, duration: p.duration ?? prior?.duration ?? null, environment: p.environment ?? '',
215
+ dateCreated: prior?.dateCreated ?? ctx.occurredAt, dateAdded: prior?.dateAdded ?? ctx.occurredAt, dateUpdated: ctx.occurredAt,
216
+ dateInProgress: prior?.dateInProgress ?? (p.status === 'in_progress' ? ctx.occurredAt : null), dateClock: ctx.occurredAt, expectedTime: prior?.expectedTime ?? ctx.occurredAt,
217
+ monitorConfig, groups: [],
218
+ }, prior ? 'check_in.update' : 'check_in.create');
219
+ await ctx.write('project', String(project.id), { hasMonitors: true }, 'project.update');
220
+ break;
221
+ }
222
+ case 'log':
223
+ for (const l of Array.isArray(p.items) ? p.items : [])
224
+ 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));
225
+ break;
226
+ case 'trace_metric':
227
+ for (const m of Array.isArray(p.items) ? p.items : [])
228
+ 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));
229
+ break;
230
+ // an item of a type the twin does not keep otherwise is skipped and retained as it came
231
+ // 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"
232
+ default: {
233
+ const blob = `envelope-item/${String(project.id)}/${envelopeN}/${n}`;
234
+ await ctx.blobs.put(blob, item.payload);
235
+ await ctx.record(OTHER_ITEM, { ...base, type, header: item.header, size: item.payload.length, blob }, id(OTHER_ITEM));
236
+ }
237
+ }
238
+ }
239
+ return Response.json(envelopeId ? { id: envelopeId } : {});
240
+ }