@volter/twin-sentry 0.1.36 → 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.
- package/README.md +21 -134
- package/api/spec/SOURCE.md +15 -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 +136 -0
- package/api/src/semantics/api.ts +246 -0
- package/api/src/semantics/around.ts +36 -0
- package/api/src/semantics/doors.ts +50 -0
- package/api/src/semantics/index.ts +19 -0
- package/api/src/semantics/shared.ts +35 -0
- package/api/src/semantics/states.ts +17 -0
- package/dist/api/spec/SOURCE.md +15 -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 +135 -0
- package/dist/api/src/manifest.ts +136 -0
- package/dist/api/src/semantics/api.d.ts +13 -0
- package/dist/api/src/semantics/api.js +267 -0
- package/dist/api/src/semantics/api.ts +246 -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 +8 -0
- package/dist/api/src/semantics/doors.js +39 -0
- package/dist/api/src/semantics/doors.ts +50 -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 +15 -0
- package/dist/api/src/semantics/shared.js +27 -0
- package/dist/api/src/semantics/shared.ts +35 -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/generated/pack-facts.json +87 -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 +34 -0
- package/dist/ingest/src/manifest.ts +39 -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 +923 -0
- package/dist/journeys/customer-life.outline.md +51 -0
- package/dist/journeys/decisions.json +170 -0
- package/dist/journeys/demand.json +136 -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 +12 -75
- package/dist/src/manifest.d.ts +2 -0
- package/dist/src/manifest.js +34 -0
- package/dist/src/semantics/shared.d.ts +87 -0
- package/dist/src/semantics/shared.js +411 -0
- package/dist/src/server.d.ts +7 -0
- package/dist/src/server.js +7 -0
- package/generated/pack-facts.json +87 -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 +39 -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 +923 -0
- package/journeys/customer-life.outline.md +51 -0
- package/journeys/decisions.json +170 -0
- package/journeys/demand.json +136 -0
- package/journeys/score.json +231 -0
- package/journeys/unreachable.json +29 -0
- package/journeys/vendor-examples.json +474 -0
- package/package.json +16 -42
- package/spec/sources.json +76 -0
- package/src/cli.ts +12 -24
- package/src/fetch.ts +10 -0
- package/src/index.ts +12 -143
- package/src/manifest.ts +40 -0
- package/src/semantics/shared.ts +443 -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,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,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,34 @@
|
|
|
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
|
+
// an envelope is not JSON: its bytes are kept (./semantics/around.ts) and read by the handler
|
|
6
|
+
body: {},
|
|
7
|
+
// 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:"
|
|
8
|
+
// The kernel decodes the declared vendor encodings before the handler reads the retained bytes.
|
|
9
|
+
encodings: { request: ['gzip', 'deflate', 'br', 'zstd'], undecodable: { status: 400, message: 'failed to read request body' } },
|
|
10
|
+
ids: { template: '{uuid}' },
|
|
11
|
+
time: 'iso',
|
|
12
|
+
// Relay's error body: `{"detail": ..., "causes": [...]}` (develop-docs/sdk/foundations/transport/index.mdx)
|
|
13
|
+
error: { detail: '{message}' },
|
|
14
|
+
readOnly: { status: 403, message: 'This twin was started read-only; envelopes are refused.' },
|
|
15
|
+
malformedBody: { status: 400, message: 'failed to read request body' },
|
|
16
|
+
notFound: { status: 404, message: 'Not Found' },
|
|
17
|
+
gap: { status: 404, message: 'Not Found' },
|
|
18
|
+
deleted: {},
|
|
19
|
+
// a browser SDK posts cross-origin: every answer allows any origin and exposes what an SDK reads of it
|
|
20
|
+
// (transport/rate-limiting.mdx: Retry-After, X-Sentry-Rate-Limits). Where the documentation stops: Relay's CORS is its
|
|
21
|
+
// code's, not a page's; the headers allowed are the ones the SDKs' transports send
|
|
22
|
+
cors: {
|
|
23
|
+
origin: '*',
|
|
24
|
+
methods: 'GET, POST, OPTIONS',
|
|
25
|
+
headers: 'x-sentry-auth, x-requested-with, x-forwarded-for, origin, referer, accept, content-type, authentication, authorization, content-encoding, transfer-encoding, sentry-trace, baggage',
|
|
26
|
+
expose: 'x-sentry-error, x-sentry-rate-limits, retry-after',
|
|
27
|
+
maxAge: 3600,
|
|
28
|
+
},
|
|
29
|
+
// the sequences the vendor numbers by (ctx.issue over each, src/semantics/shared.ts next)
|
|
30
|
+
// All stored vendor resources are owned and refreshed by the API manifest through ctx.over.
|
|
31
|
+
resources: {},
|
|
32
|
+
vendorBacked: { none: 'Envelope receipt is telemetry ingestion, never deployed as account changes. The API manifest owns and refreshes its resulting vendor resources.' },
|
|
33
|
+
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' },
|
|
34
|
+
};
|
|
@@ -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,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
|
+
}
|