@terreno/api 0.13.3 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/versionCheckPlugin.test.js +136 -3
- package/dist/api.arrayOperations.test.js +1 -0
- package/dist/api.d.ts +15 -4
- package/dist/api.errors.test.js +1 -0
- package/dist/api.hooks.test.js +1 -0
- package/dist/api.js +153 -104
- package/dist/api.query.test.js +1 -0
- package/dist/api.test.js +174 -0
- package/dist/auth.d.ts +10 -5
- package/dist/auth.js +163 -90
- package/dist/auth.test.js +159 -0
- package/dist/betterAuthApp.test.js +1 -0
- package/dist/betterAuthSetup.d.ts +5 -6
- package/dist/betterAuthSetup.js +30 -17
- package/dist/betterAuthSetup.test.js +1 -0
- package/dist/config.d.ts +48 -0
- package/dist/config.js +257 -0
- package/dist/config.test.d.ts +1 -0
- package/dist/config.test.js +328 -0
- package/dist/configuration.test.js +1 -0
- package/dist/configurationApp.d.ts +1 -1
- package/dist/configurationApp.js +17 -13
- package/dist/configurationPlugin.test.js +1 -0
- package/dist/consentApp.test.js +1 -0
- package/dist/envConfigurationPlugin.d.ts +2 -0
- package/dist/envConfigurationPlugin.js +173 -0
- package/dist/envConfigurationPlugin.test.d.ts +1 -0
- package/dist/envConfigurationPlugin.test.js +322 -0
- package/dist/errors.d.ts +18 -7
- package/dist/errors.js +111 -12
- package/dist/errors.test.js +16 -1
- package/dist/example.js +19 -7
- package/dist/expressServer.d.ts +10 -9
- package/dist/expressServer.js +62 -53
- package/dist/expressServer.test.js +165 -2
- package/dist/githubAuth.d.ts +2 -1
- package/dist/githubAuth.js +41 -26
- package/dist/githubAuth.test.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/logger.d.ts +1 -1
- package/dist/logger.js +42 -20
- package/dist/models/versionConfig.d.ts +2 -0
- package/dist/models/versionConfig.js +8 -0
- package/dist/notifiers/googleChatNotifier.js +14 -16
- package/dist/notifiers/googleChatNotifier.test.js +1 -0
- package/dist/notifiers/slackNotifier.js +16 -14
- package/dist/notifiers/slackNotifier.test.js +41 -3
- package/dist/notifiers/zoomNotifier.js +7 -10
- package/dist/notifiers/zoomNotifier.test.js +1 -0
- package/dist/openApi.d.ts +1 -1
- package/dist/openApi.test.js +1 -0
- package/dist/openApiBuilder.d.ts +39 -6
- package/dist/openApiBuilder.js +1 -31
- package/dist/openApiBuilder.test.js +1 -0
- package/dist/openApiValidator.js +1 -0
- package/dist/openApiValidator.test.js +1 -0
- package/dist/permissions.d.ts +4 -4
- package/dist/permissions.js +67 -65
- package/dist/permissions.middleware.test.js +1 -0
- package/dist/permissions.test.js +1 -0
- package/dist/plugins.d.ts +5 -5
- package/dist/plugins.js +18 -9
- package/dist/plugins.test.js +1 -1
- package/dist/populate.d.ts +15 -8
- package/dist/populate.js +23 -24
- package/dist/populate.test.js +1 -0
- package/dist/realtime/changeStreamWatcher.d.ts +73 -0
- package/dist/realtime/changeStreamWatcher.js +724 -0
- package/dist/realtime/index.d.ts +6 -0
- package/dist/realtime/index.js +27 -0
- package/dist/realtime/queryMatcher.d.ts +14 -0
- package/dist/realtime/queryMatcher.js +250 -0
- package/dist/realtime/queryStore.d.ts +37 -0
- package/dist/realtime/queryStore.js +195 -0
- package/dist/realtime/realtime.test.d.ts +10 -0
- package/dist/realtime/realtime.test.js +3066 -0
- package/dist/realtime/realtimeApp.d.ts +93 -0
- package/dist/realtime/realtimeApp.js +560 -0
- package/dist/realtime/registry.d.ts +40 -0
- package/dist/realtime/registry.js +38 -0
- package/dist/realtime/socketUser.d.ts +10 -0
- package/dist/realtime/socketUser.js +17 -0
- package/dist/realtime/types.d.ts +100 -0
- package/dist/realtime/types.js +2 -0
- package/dist/requestContext.d.ts +37 -0
- package/dist/requestContext.js +344 -0
- package/dist/requestContext.test.d.ts +1 -0
- package/dist/requestContext.test.js +384 -0
- package/dist/terrenoApp.d.ts +8 -0
- package/dist/terrenoApp.js +50 -13
- package/dist/terrenoApp.test.js +194 -21
- package/dist/terrenoPlugin.d.ts +11 -0
- package/dist/tests/bunSetup.js +1 -0
- package/dist/tests.js +1 -1
- package/dist/transformers.d.ts +2 -2
- package/dist/transformers.js +5 -3
- package/dist/transformers.test.js +90 -0
- package/dist/types/consentResponse.d.ts +6 -3
- package/dist/versionCheckPlugin.d.ts +2 -0
- package/dist/versionCheckPlugin.js +18 -12
- package/package.json +4 -2
- package/src/__tests__/versionCheckPlugin.test.ts +94 -3
- package/src/api.arrayOperations.test.ts +1 -0
- package/src/api.errors.test.ts +1 -0
- package/src/api.hooks.test.ts +1 -0
- package/src/api.query.test.ts +1 -0
- package/src/api.test.ts +132 -0
- package/src/api.ts +199 -84
- package/src/auth.test.ts +160 -0
- package/src/auth.ts +120 -50
- package/src/betterAuthApp.test.ts +1 -0
- package/src/betterAuthSetup.test.ts +1 -0
- package/src/betterAuthSetup.ts +59 -22
- package/src/config.test.ts +255 -0
- package/src/config.ts +216 -0
- package/src/configuration.test.ts +1 -0
- package/src/configurationApp.ts +59 -24
- package/src/configurationPlugin.test.ts +1 -0
- package/src/consentApp.test.ts +1 -0
- package/src/envConfigurationPlugin.test.ts +143 -0
- package/src/envConfigurationPlugin.ts +100 -0
- package/src/errors.test.ts +19 -1
- package/src/errors.ts +118 -38
- package/src/example.ts +49 -21
- package/src/express.d.ts +18 -1
- package/src/expressServer.test.ts +147 -2
- package/src/expressServer.ts +80 -50
- package/src/githubAuth.test.ts +1 -0
- package/src/githubAuth.ts +59 -38
- package/src/index.ts +4 -0
- package/src/logger.ts +47 -17
- package/src/models/versionConfig.ts +13 -2
- package/src/notifiers/googleChatNotifier.test.ts +1 -0
- package/src/notifiers/googleChatNotifier.ts +7 -9
- package/src/notifiers/slackNotifier.test.ts +29 -3
- package/src/notifiers/slackNotifier.ts +9 -7
- package/src/notifiers/zoomNotifier.test.ts +1 -0
- package/src/notifiers/zoomNotifier.ts +8 -11
- package/src/openApi.test.ts +1 -0
- package/src/openApi.ts +4 -4
- package/src/openApiBuilder.test.ts +1 -0
- package/src/openApiBuilder.ts +14 -11
- package/src/openApiValidator.test.ts +1 -0
- package/src/openApiValidator.ts +3 -2
- package/src/permissions.middleware.test.ts +1 -0
- package/src/permissions.test.ts +1 -0
- package/src/permissions.ts +30 -25
- package/src/plugins.test.ts +1 -1
- package/src/plugins.ts +21 -14
- package/src/populate.test.ts +1 -0
- package/src/populate.ts +44 -36
- package/src/realtime/changeStreamWatcher.ts +572 -0
- package/src/realtime/index.ts +34 -0
- package/src/realtime/queryMatcher.ts +179 -0
- package/src/realtime/queryStore.ts +132 -0
- package/src/realtime/realtime.test.ts +2465 -0
- package/src/realtime/realtimeApp.ts +478 -0
- package/src/realtime/registry.ts +64 -0
- package/src/realtime/socketUser.ts +25 -0
- package/src/realtime/types.ts +112 -0
- package/src/requestContext.test.ts +321 -0
- package/src/requestContext.ts +368 -0
- package/src/terrenoApp.test.ts +137 -11
- package/src/terrenoApp.ts +64 -17
- package/src/terrenoPlugin.ts +12 -0
- package/src/tests/bunSetup.ts +1 -0
- package/src/tests.ts +7 -2
- package/src/transformers.test.ts +70 -2
- package/src/transformers.ts +15 -7
- package/src/types/consentResponse.ts +8 -10
- package/src/versionCheckPlugin.ts +15 -7
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import {afterEach, beforeEach, describe, expect, it, type Mock} from "bun:test";
|
|
2
|
+
import {Writable} from "node:stream";
|
|
3
|
+
import * as Sentry from "@sentry/bun";
|
|
4
|
+
import express from "express";
|
|
5
|
+
import supertest from "supertest";
|
|
6
|
+
import winston from "winston";
|
|
7
|
+
|
|
8
|
+
import {logger, setupLogging} from "./logger";
|
|
9
|
+
import {
|
|
10
|
+
getCurrentLogContext,
|
|
11
|
+
getCurrentRequestContext,
|
|
12
|
+
getCurrentRequestContextAttributes,
|
|
13
|
+
getRequestContextFromAttributes,
|
|
14
|
+
requestContextMiddleware,
|
|
15
|
+
runWithRequestContext,
|
|
16
|
+
runWithRequestContextAttributes,
|
|
17
|
+
setRequestContext,
|
|
18
|
+
} from "./requestContext";
|
|
19
|
+
|
|
20
|
+
describe("request context job propagation", () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
const scope = Sentry.getCurrentScope();
|
|
23
|
+
(scope.setContext as unknown as Mock<(...args: unknown[]) => void>).mockClear();
|
|
24
|
+
(scope.setTag as unknown as Mock<(...args: unknown[]) => void>).mockClear();
|
|
25
|
+
(scope.setUser as unknown as Mock<(...args: unknown[]) => void>).mockClear();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
setupLogging({disableFileLogging: true});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("serializes the current context for downstream jobs", () => {
|
|
33
|
+
runWithRequestContext(
|
|
34
|
+
{
|
|
35
|
+
jobId: "job-1",
|
|
36
|
+
requestId: "request-1",
|
|
37
|
+
sessionId: "session-1",
|
|
38
|
+
spanId: "span-1",
|
|
39
|
+
traceId: "trace-1",
|
|
40
|
+
traceSampled: false,
|
|
41
|
+
userId: "user-1",
|
|
42
|
+
},
|
|
43
|
+
() => {
|
|
44
|
+
expect(getCurrentRequestContextAttributes()).toEqual({
|
|
45
|
+
"x-job-id": "job-1",
|
|
46
|
+
"x-request-id": "request-1",
|
|
47
|
+
"x-session-id": "session-1",
|
|
48
|
+
"x-span-id": "span-1",
|
|
49
|
+
"x-trace-id": "trace-1",
|
|
50
|
+
"x-trace-sampled": "false",
|
|
51
|
+
"x-user-id": "user-1",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("allows downstream jobs to replace only the job id", () => {
|
|
58
|
+
runWithRequestContext(
|
|
59
|
+
{jobId: "job-parent", requestId: "request-1", sessionId: "session-1"},
|
|
60
|
+
() => {
|
|
61
|
+
expect(getCurrentRequestContextAttributes({jobId: "job-child"})).toEqual({
|
|
62
|
+
"x-job-id": "job-child",
|
|
63
|
+
"x-request-id": "request-1",
|
|
64
|
+
"x-session-id": "session-1",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("restores worker context from message attributes", () => {
|
|
71
|
+
const scope = Sentry.getCurrentScope();
|
|
72
|
+
const setContextMock = scope.setContext as unknown as Mock<(...args: unknown[]) => void>;
|
|
73
|
+
const setTagMock = scope.setTag as unknown as Mock<(...args: unknown[]) => void>;
|
|
74
|
+
const setUserMock = scope.setUser as unknown as Mock<(...args: unknown[]) => void>;
|
|
75
|
+
|
|
76
|
+
runWithRequestContextAttributes(
|
|
77
|
+
{
|
|
78
|
+
traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
|
|
79
|
+
"x-job-id": "job-worker-1",
|
|
80
|
+
"x-request-id": "request-1",
|
|
81
|
+
"x-session-id": "session-1",
|
|
82
|
+
"x-user-id": "user-1",
|
|
83
|
+
},
|
|
84
|
+
() => {
|
|
85
|
+
expect(getCurrentRequestContext()).toEqual({
|
|
86
|
+
jobId: "job-worker-1",
|
|
87
|
+
requestId: "request-1",
|
|
88
|
+
sessionId: "session-1",
|
|
89
|
+
spanId: "00f067aa0ba902b7",
|
|
90
|
+
traceId: "4bf92f3577b34da6a3ce929d0e0e4736",
|
|
91
|
+
traceSampled: true,
|
|
92
|
+
userId: "user-1",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
expect(setTagMock).toHaveBeenCalledWith("request_id", "request-1");
|
|
98
|
+
expect(setTagMock).toHaveBeenCalledWith("session_id", "session-1");
|
|
99
|
+
expect(setTagMock).toHaveBeenCalledWith("job_id", "job-worker-1");
|
|
100
|
+
expect(setTagMock).toHaveBeenCalledWith("user_id", "user-1");
|
|
101
|
+
expect(setTagMock).toHaveBeenCalledWith("trace_id", "4bf92f3577b34da6a3ce929d0e0e4736");
|
|
102
|
+
expect(setUserMock).toHaveBeenCalledWith({id: "user-1"});
|
|
103
|
+
expect(setContextMock).toHaveBeenCalledWith("request_context", {
|
|
104
|
+
jobId: "job-worker-1",
|
|
105
|
+
requestId: "request-1",
|
|
106
|
+
sessionId: "session-1",
|
|
107
|
+
spanId: "00f067aa0ba902b7",
|
|
108
|
+
traceId: "4bf92f3577b34da6a3ce929d0e0e4736",
|
|
109
|
+
traceSampled: true,
|
|
110
|
+
userId: "user-1",
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("sends context updates to Sentry for auth session changes", () => {
|
|
115
|
+
const scope = Sentry.getCurrentScope();
|
|
116
|
+
const setTagMock = scope.setTag as unknown as Mock<(...args: unknown[]) => void>;
|
|
117
|
+
const setUserMock = scope.setUser as unknown as Mock<(...args: unknown[]) => void>;
|
|
118
|
+
|
|
119
|
+
runWithRequestContext({requestId: "request-auth-1"}, () => {
|
|
120
|
+
setTagMock.mockClear();
|
|
121
|
+
setUserMock.mockClear();
|
|
122
|
+
setRequestContext({sessionId: "session-auth-1", userId: "user-auth-1"});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(setTagMock).toHaveBeenCalledWith("session_id", "session-auth-1");
|
|
126
|
+
expect(setTagMock).toHaveBeenCalledWith("user_id", "user-auth-1");
|
|
127
|
+
expect(setUserMock).toHaveBeenCalledWith({id: "user-auth-1"});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("uses trace id as request id when attributes do not include request id", () => {
|
|
131
|
+
const context = getRequestContextFromAttributes({
|
|
132
|
+
traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00",
|
|
133
|
+
"x-job-id": "job-worker-1",
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(context).toEqual({
|
|
137
|
+
jobId: "job-worker-1",
|
|
138
|
+
requestId: "4bf92f3577b34da6a3ce929d0e0e4736",
|
|
139
|
+
sessionId: undefined,
|
|
140
|
+
spanId: "00f067aa0ba902b7",
|
|
141
|
+
traceId: "4bf92f3577b34da6a3ce929d0e0e4736",
|
|
142
|
+
traceSampled: false,
|
|
143
|
+
userId: undefined,
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("accepts job id from HTTP headers", async () => {
|
|
148
|
+
const app = express();
|
|
149
|
+
app.use(requestContextMiddleware);
|
|
150
|
+
app.get("/job", (req, res) => {
|
|
151
|
+
res.json({context: getCurrentLogContext(), jobId: req.jobId});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const res = await supertest(app)
|
|
155
|
+
.get("/job")
|
|
156
|
+
.set("X-Job-ID", "job-http-1")
|
|
157
|
+
.set("X-Request-ID", "request-http-1")
|
|
158
|
+
.expect(200);
|
|
159
|
+
|
|
160
|
+
expect(res.headers["x-job-id"]).toBe("job-http-1");
|
|
161
|
+
expect(res.body).toEqual({
|
|
162
|
+
context: {jobId: "job-http-1", requestId: "request-http-1"},
|
|
163
|
+
jobId: "job-http-1",
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("parses Google Cloud trace context header in middleware", async () => {
|
|
168
|
+
const app = express();
|
|
169
|
+
app.use(requestContextMiddleware);
|
|
170
|
+
app.get("/trace-gcloud", (_req, res) => {
|
|
171
|
+
const ctx = getCurrentRequestContext();
|
|
172
|
+
res.json({
|
|
173
|
+
spanId: ctx?.spanId,
|
|
174
|
+
traceId: ctx?.traceId,
|
|
175
|
+
traceSampled: ctx?.traceSampled,
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const res = await supertest(app)
|
|
180
|
+
.get("/trace-gcloud")
|
|
181
|
+
.set("X-Cloud-Trace-Context", "105445aa7843bc8bf206b12000100000/1;o=1")
|
|
182
|
+
.expect(200);
|
|
183
|
+
|
|
184
|
+
expect(res.body.traceId).toBe("105445aa7843bc8bf206b12000100000");
|
|
185
|
+
expect(res.body.spanId).toBe("1");
|
|
186
|
+
expect(res.body.traceSampled).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("parses Google Cloud trace context without trace sampling", async () => {
|
|
190
|
+
const app = express();
|
|
191
|
+
app.use(requestContextMiddleware);
|
|
192
|
+
app.get("/trace-gcloud-nosample", (_req, res) => {
|
|
193
|
+
const ctx = getCurrentRequestContext();
|
|
194
|
+
res.json({
|
|
195
|
+
spanId: ctx?.spanId,
|
|
196
|
+
traceId: ctx?.traceId,
|
|
197
|
+
traceSampled: ctx?.traceSampled,
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const res = await supertest(app)
|
|
202
|
+
.get("/trace-gcloud-nosample")
|
|
203
|
+
.set("X-Cloud-Trace-Context", "abc123/42;o=0")
|
|
204
|
+
.expect(200);
|
|
205
|
+
|
|
206
|
+
expect(res.body.traceId).toBe("abc123");
|
|
207
|
+
expect(res.body.spanId).toBe("42");
|
|
208
|
+
expect(res.body.traceSampled).toBe(false);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("falls back to traceparent when cloud trace context is absent", async () => {
|
|
212
|
+
const app = express();
|
|
213
|
+
app.use(requestContextMiddleware);
|
|
214
|
+
app.get("/trace-parent", (_req, res) => {
|
|
215
|
+
const ctx = getCurrentRequestContext();
|
|
216
|
+
res.json({
|
|
217
|
+
spanId: ctx?.spanId,
|
|
218
|
+
traceId: ctx?.traceId,
|
|
219
|
+
traceSampled: ctx?.traceSampled,
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const res = await supertest(app)
|
|
224
|
+
.get("/trace-parent")
|
|
225
|
+
.set("traceparent", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")
|
|
226
|
+
.expect(200);
|
|
227
|
+
|
|
228
|
+
expect(res.body.traceId).toBe("4bf92f3577b34da6a3ce929d0e0e4736");
|
|
229
|
+
expect(res.body.spanId).toBe("00f067aa0ba902b7");
|
|
230
|
+
expect(res.body.traceSampled).toBe(true);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("uses trace id as request id when no explicit request id header is set", async () => {
|
|
234
|
+
const app = express();
|
|
235
|
+
app.use(requestContextMiddleware);
|
|
236
|
+
app.get("/trace-request-id", (_req, res) => {
|
|
237
|
+
const ctx = getCurrentRequestContext();
|
|
238
|
+
res.json({requestId: ctx?.requestId});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const res = await supertest(app)
|
|
242
|
+
.get("/trace-request-id")
|
|
243
|
+
.set("X-Cloud-Trace-Context", "trace-as-rid/99;o=1")
|
|
244
|
+
.expect(200);
|
|
245
|
+
|
|
246
|
+
expect(res.body.requestId).toBe("trace-as-rid");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("handles traceSampled attribute values 'true', '1', 'false', '0'", () => {
|
|
250
|
+
const ctxTrue = getRequestContextFromAttributes({
|
|
251
|
+
"x-request-id": "r1",
|
|
252
|
+
"x-trace-sampled": "true",
|
|
253
|
+
});
|
|
254
|
+
expect(ctxTrue.traceSampled).toBe(true);
|
|
255
|
+
|
|
256
|
+
const ctx1 = getRequestContextFromAttributes({
|
|
257
|
+
"x-request-id": "r2",
|
|
258
|
+
"x-trace-sampled": "1",
|
|
259
|
+
});
|
|
260
|
+
expect(ctx1.traceSampled).toBe(true);
|
|
261
|
+
|
|
262
|
+
const ctxFalse = getRequestContextFromAttributes({
|
|
263
|
+
"x-request-id": "r3",
|
|
264
|
+
"x-trace-sampled": "false",
|
|
265
|
+
});
|
|
266
|
+
expect(ctxFalse.traceSampled).toBe(false);
|
|
267
|
+
|
|
268
|
+
const ctx0 = getRequestContextFromAttributes({
|
|
269
|
+
"x-request-id": "r4",
|
|
270
|
+
"x-trace-sampled": "0",
|
|
271
|
+
});
|
|
272
|
+
expect(ctx0.traceSampled).toBe(false);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("parses Google Cloud trace context with missing span id", () => {
|
|
276
|
+
const ctx = getRequestContextFromAttributes({
|
|
277
|
+
"x-cloud-trace-context": "only-trace-id",
|
|
278
|
+
"x-request-id": "r5",
|
|
279
|
+
});
|
|
280
|
+
expect(ctx.traceId).toBe("only-trace-id");
|
|
281
|
+
expect(ctx.spanId).toBeUndefined();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("returns undefined trace when traceparent has empty trace id", () => {
|
|
285
|
+
const ctx = getRequestContextFromAttributes({
|
|
286
|
+
traceparent: "00--span-01",
|
|
287
|
+
"x-request-id": "r6",
|
|
288
|
+
});
|
|
289
|
+
expect(ctx.traceId).toBeUndefined();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("adds job id to logger context", () => {
|
|
293
|
+
let output = "";
|
|
294
|
+
const stream = new Writable({
|
|
295
|
+
write: (chunk, _encoding, callback): void => {
|
|
296
|
+
output += chunk.toString();
|
|
297
|
+
callback();
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
setupLogging({
|
|
302
|
+
disableConsoleLogging: true,
|
|
303
|
+
disableFileLogging: true,
|
|
304
|
+
transports: [
|
|
305
|
+
new winston.transports.Stream({
|
|
306
|
+
format: winston.format.printf((info) => {
|
|
307
|
+
return `${info.level}: ${info.message} requestId=${info.requestId} jobId=${info.jobId}`;
|
|
308
|
+
}),
|
|
309
|
+
stream,
|
|
310
|
+
}),
|
|
311
|
+
],
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
runWithRequestContext({jobId: "job-log-1", requestId: "request-log-1"}, () => {
|
|
315
|
+
logger.info("worker handled job");
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
expect(output).toContain("requestId=request-log-1");
|
|
319
|
+
expect(output).toContain("jobId=job-log-1");
|
|
320
|
+
});
|
|
321
|
+
});
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import {AsyncLocalStorage} from "node:async_hooks";
|
|
2
|
+
import {randomUUID} from "node:crypto";
|
|
3
|
+
import * as Sentry from "@sentry/bun";
|
|
4
|
+
import type express from "express";
|
|
5
|
+
import type {JwtPayload} from "jsonwebtoken";
|
|
6
|
+
|
|
7
|
+
const CLOUD_TRACE_CONTEXT_HEADER = "x-cloud-trace-context";
|
|
8
|
+
const JOB_ID_HEADER = "x-job-id";
|
|
9
|
+
const REQUEST_ID_HEADERS = ["x-request-id", "x-correlation-id", "x-transaction-id"];
|
|
10
|
+
const SESSION_ID_HEADER = "x-session-id";
|
|
11
|
+
const SPAN_ID_HEADER = "x-span-id";
|
|
12
|
+
const TRACE_ID_HEADER = "x-trace-id";
|
|
13
|
+
const TRACE_PARENT_HEADER = "traceparent";
|
|
14
|
+
const TRACE_SAMPLED_HEADER = "x-trace-sampled";
|
|
15
|
+
const USER_ID_HEADER = "x-user-id";
|
|
16
|
+
|
|
17
|
+
export interface RequestContext {
|
|
18
|
+
jobId?: string;
|
|
19
|
+
requestId: string;
|
|
20
|
+
sessionId?: string;
|
|
21
|
+
spanId?: string;
|
|
22
|
+
traceId?: string;
|
|
23
|
+
traceSampled?: boolean;
|
|
24
|
+
userId?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type RequestContextAttributes = Record<string, string>;
|
|
28
|
+
|
|
29
|
+
export const REQUEST_CONTEXT_ATTRIBUTE_NAMES = {
|
|
30
|
+
jobId: JOB_ID_HEADER,
|
|
31
|
+
requestId: "x-request-id",
|
|
32
|
+
sessionId: SESSION_ID_HEADER,
|
|
33
|
+
spanId: SPAN_ID_HEADER,
|
|
34
|
+
traceId: TRACE_ID_HEADER,
|
|
35
|
+
traceParent: TRACE_PARENT_HEADER,
|
|
36
|
+
traceSampled: TRACE_SAMPLED_HEADER,
|
|
37
|
+
userId: USER_ID_HEADER,
|
|
38
|
+
} as const;
|
|
39
|
+
|
|
40
|
+
interface GoogleCloudTraceContext {
|
|
41
|
+
spanId?: string;
|
|
42
|
+
traceId?: string;
|
|
43
|
+
traceSampled?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface JwtSessionPayload extends JwtPayload {
|
|
47
|
+
sid?: string;
|
|
48
|
+
sessionId?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const requestContextStorage = new AsyncLocalStorage<RequestContext>();
|
|
52
|
+
|
|
53
|
+
const getHeader = (req: express.Request, headerName: string): string | undefined => {
|
|
54
|
+
const value = req.header(headerName);
|
|
55
|
+
if (!Array.isArray(value)) {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
return value[0];
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const parseGoogleCloudTraceContext = (
|
|
62
|
+
headerValue?: string
|
|
63
|
+
): GoogleCloudTraceContext | undefined => {
|
|
64
|
+
if (!headerValue) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const [traceAndSpan, options] = headerValue.split(";");
|
|
69
|
+
const [traceId, spanId] = traceAndSpan.split("/");
|
|
70
|
+
if (!traceId) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
spanId,
|
|
76
|
+
traceId,
|
|
77
|
+
traceSampled: options === "o=1",
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const parseTraceParent = (headerValue?: string): GoogleCloudTraceContext | undefined => {
|
|
82
|
+
if (!headerValue) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const [_version, traceId, spanId, flags] = headerValue.split("-");
|
|
87
|
+
if (!traceId) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
spanId,
|
|
93
|
+
traceId,
|
|
94
|
+
traceSampled: flags === "01",
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const parseTraceSampled = (value?: string | boolean): boolean | undefined => {
|
|
99
|
+
if (typeof value === "boolean") {
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
if (value === "true" || value === "1") {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
if (value === "false" || value === "0") {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const getIncomingRequestId = (
|
|
112
|
+
req: express.Request,
|
|
113
|
+
traceContext?: GoogleCloudTraceContext
|
|
114
|
+
): string => {
|
|
115
|
+
for (const headerName of REQUEST_ID_HEADERS) {
|
|
116
|
+
const headerValue = getHeader(req, headerName);
|
|
117
|
+
if (headerValue) {
|
|
118
|
+
return headerValue;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (traceContext?.traceId) {
|
|
123
|
+
return traceContext.traceId;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return randomUUID();
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const getSessionIdFromJwtPayload = (
|
|
130
|
+
payload?: JwtSessionPayload | null
|
|
131
|
+
): string | undefined => {
|
|
132
|
+
if (!payload) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
if (payload.sid) {
|
|
136
|
+
return payload.sid;
|
|
137
|
+
}
|
|
138
|
+
return payload.sessionId;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const getRequestContextFromAttributes = (
|
|
142
|
+
attributes: Record<string, string | undefined> = {}
|
|
143
|
+
): RequestContext => {
|
|
144
|
+
const cloudTraceContext = parseGoogleCloudTraceContext(attributes[CLOUD_TRACE_CONTEXT_HEADER]);
|
|
145
|
+
const traceParentContext = parseTraceParent(attributes[TRACE_PARENT_HEADER]);
|
|
146
|
+
const traceContext = cloudTraceContext ?? traceParentContext;
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
jobId: attributes[JOB_ID_HEADER],
|
|
150
|
+
requestId:
|
|
151
|
+
attributes[REQUEST_CONTEXT_ATTRIBUTE_NAMES.requestId] ??
|
|
152
|
+
attributes["x-correlation-id"] ??
|
|
153
|
+
attributes["x-transaction-id"] ??
|
|
154
|
+
traceContext?.traceId ??
|
|
155
|
+
randomUUID(),
|
|
156
|
+
sessionId: attributes[SESSION_ID_HEADER],
|
|
157
|
+
spanId: attributes[SPAN_ID_HEADER] ?? traceContext?.spanId,
|
|
158
|
+
traceId: attributes[TRACE_ID_HEADER] ?? traceContext?.traceId,
|
|
159
|
+
traceSampled: parseTraceSampled(attributes[TRACE_SAMPLED_HEADER]) ?? traceContext?.traceSampled,
|
|
160
|
+
userId: attributes[USER_ID_HEADER],
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const getCurrentRequestContext = (): RequestContext | undefined => {
|
|
165
|
+
return requestContextStorage.getStore();
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export const getCurrentLogContext = (): Partial<RequestContext> => {
|
|
169
|
+
const context = getCurrentRequestContext();
|
|
170
|
+
if (!context) {
|
|
171
|
+
return {};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
jobId: context.jobId,
|
|
176
|
+
requestId: context.requestId,
|
|
177
|
+
sessionId: context.sessionId,
|
|
178
|
+
spanId: context.spanId,
|
|
179
|
+
traceId: context.traceId,
|
|
180
|
+
traceSampled: context.traceSampled,
|
|
181
|
+
userId: context.userId,
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const setSentryTag = (
|
|
186
|
+
scope: ReturnType<typeof Sentry.getCurrentScope>,
|
|
187
|
+
name: string,
|
|
188
|
+
value?: string | boolean
|
|
189
|
+
): void => {
|
|
190
|
+
if (typeof value === "undefined") {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
scope.setTag(name, String(value));
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const setSentryContextValue = (
|
|
197
|
+
context: Record<string, string | boolean>,
|
|
198
|
+
name: string,
|
|
199
|
+
value?: string | boolean
|
|
200
|
+
): void => {
|
|
201
|
+
if (typeof value === "undefined") {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
context[name] = value;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export const applyRequestContextToSentry = (
|
|
208
|
+
context: Partial<RequestContext> = getCurrentLogContext()
|
|
209
|
+
): void => {
|
|
210
|
+
const scope = Sentry.getCurrentScope();
|
|
211
|
+
setSentryTag(scope, "request_id", context.requestId);
|
|
212
|
+
setSentryTag(scope, "session_id", context.sessionId);
|
|
213
|
+
setSentryTag(scope, "job_id", context.jobId);
|
|
214
|
+
setSentryTag(scope, "user_id", context.userId);
|
|
215
|
+
setSentryTag(scope, "trace_id", context.traceId);
|
|
216
|
+
setSentryTag(scope, "span_id", context.spanId);
|
|
217
|
+
setSentryTag(scope, "trace_sampled", context.traceSampled);
|
|
218
|
+
|
|
219
|
+
if (context.userId) {
|
|
220
|
+
scope.setUser({id: context.userId});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const sentryContext: Record<string, string | boolean> = {};
|
|
224
|
+
setSentryContextValue(sentryContext, "requestId", context.requestId);
|
|
225
|
+
setSentryContextValue(sentryContext, "sessionId", context.sessionId);
|
|
226
|
+
setSentryContextValue(sentryContext, "jobId", context.jobId);
|
|
227
|
+
setSentryContextValue(sentryContext, "userId", context.userId);
|
|
228
|
+
setSentryContextValue(sentryContext, "traceId", context.traceId);
|
|
229
|
+
setSentryContextValue(sentryContext, "spanId", context.spanId);
|
|
230
|
+
setSentryContextValue(sentryContext, "traceSampled", context.traceSampled);
|
|
231
|
+
|
|
232
|
+
if (Object.keys(sentryContext).length > 0) {
|
|
233
|
+
scope.setContext("request_context", sentryContext);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export const setRequestContext = (updates: Partial<RequestContext>): void => {
|
|
238
|
+
const context = getCurrentRequestContext();
|
|
239
|
+
if (!context) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
Object.assign(context, updates);
|
|
243
|
+
applyRequestContextToSentry(context);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const setAttribute = (
|
|
247
|
+
attributes: RequestContextAttributes,
|
|
248
|
+
name: string,
|
|
249
|
+
value?: string | boolean
|
|
250
|
+
): void => {
|
|
251
|
+
if (typeof value === "undefined") {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
attributes[name] = String(value);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
export const getCurrentRequestContextAttributes = (
|
|
258
|
+
overrides: Partial<RequestContext> = {}
|
|
259
|
+
): RequestContextAttributes => {
|
|
260
|
+
const context = {...getCurrentLogContext(), ...overrides};
|
|
261
|
+
const attributes: RequestContextAttributes = {};
|
|
262
|
+
setAttribute(attributes, REQUEST_CONTEXT_ATTRIBUTE_NAMES.requestId, context.requestId);
|
|
263
|
+
setAttribute(attributes, REQUEST_CONTEXT_ATTRIBUTE_NAMES.sessionId, context.sessionId);
|
|
264
|
+
setAttribute(attributes, REQUEST_CONTEXT_ATTRIBUTE_NAMES.jobId, context.jobId);
|
|
265
|
+
setAttribute(attributes, REQUEST_CONTEXT_ATTRIBUTE_NAMES.userId, context.userId);
|
|
266
|
+
setAttribute(attributes, REQUEST_CONTEXT_ATTRIBUTE_NAMES.traceId, context.traceId);
|
|
267
|
+
setAttribute(attributes, REQUEST_CONTEXT_ATTRIBUTE_NAMES.spanId, context.spanId);
|
|
268
|
+
setAttribute(attributes, REQUEST_CONTEXT_ATTRIBUTE_NAMES.traceSampled, context.traceSampled);
|
|
269
|
+
return attributes;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
export const runWithRequestContext = <T>(
|
|
273
|
+
context: Partial<RequestContext>,
|
|
274
|
+
callback: () => T
|
|
275
|
+
): T => {
|
|
276
|
+
const nextContext = {
|
|
277
|
+
...context,
|
|
278
|
+
requestId: context.requestId ?? randomUUID(),
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
return requestContextStorage.run(nextContext, () => {
|
|
282
|
+
applyRequestContextToSentry(nextContext);
|
|
283
|
+
return callback();
|
|
284
|
+
});
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
export const runWithRequestContextAttributes = <T>(
|
|
288
|
+
attributes: Record<string, string | undefined> = {},
|
|
289
|
+
callback: () => T
|
|
290
|
+
): T => {
|
|
291
|
+
return runWithRequestContext(getRequestContextFromAttributes(attributes), callback);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
export const updateRequestContextFromRequest = (
|
|
295
|
+
req: express.Request,
|
|
296
|
+
res?: express.Response
|
|
297
|
+
): void => {
|
|
298
|
+
const reqWithContext = req as express.Request & {
|
|
299
|
+
authTokenPayload?: JwtSessionPayload;
|
|
300
|
+
betterAuthSession?: {session?: {id?: string}};
|
|
301
|
+
jobId?: string;
|
|
302
|
+
requestId?: string;
|
|
303
|
+
sessionId?: string;
|
|
304
|
+
};
|
|
305
|
+
const jobId = reqWithContext.jobId ?? getHeader(req, JOB_ID_HEADER);
|
|
306
|
+
const sessionId =
|
|
307
|
+
getSessionIdFromJwtPayload(reqWithContext.authTokenPayload) ??
|
|
308
|
+
reqWithContext.betterAuthSession?.session?.id ??
|
|
309
|
+
reqWithContext.sessionId ??
|
|
310
|
+
getHeader(req, SESSION_ID_HEADER);
|
|
311
|
+
const user = req.user as {_id?: unknown; id?: string} | undefined;
|
|
312
|
+
const userId = user?.id ?? (user?._id ? String(user._id) : undefined);
|
|
313
|
+
|
|
314
|
+
setRequestContext({jobId, sessionId, userId});
|
|
315
|
+
|
|
316
|
+
if (jobId) {
|
|
317
|
+
reqWithContext.jobId = jobId;
|
|
318
|
+
res?.setHeader("X-Job-ID", jobId);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (sessionId) {
|
|
322
|
+
reqWithContext.sessionId = sessionId;
|
|
323
|
+
res?.setHeader("X-Session-ID", sessionId);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
export const requestContextMiddleware = (
|
|
328
|
+
req: express.Request,
|
|
329
|
+
res: express.Response,
|
|
330
|
+
next: express.NextFunction
|
|
331
|
+
): void => {
|
|
332
|
+
const cloudTraceContext = parseGoogleCloudTraceContext(
|
|
333
|
+
getHeader(req, CLOUD_TRACE_CONTEXT_HEADER)
|
|
334
|
+
);
|
|
335
|
+
const traceParentContext = parseTraceParent(getHeader(req, TRACE_PARENT_HEADER));
|
|
336
|
+
const traceContext = cloudTraceContext ?? traceParentContext;
|
|
337
|
+
const requestId = getIncomingRequestId(req, traceContext);
|
|
338
|
+
const jobId = getHeader(req, JOB_ID_HEADER);
|
|
339
|
+
const sessionId = getHeader(req, SESSION_ID_HEADER);
|
|
340
|
+
|
|
341
|
+
const context: RequestContext = {
|
|
342
|
+
jobId,
|
|
343
|
+
requestId,
|
|
344
|
+
sessionId,
|
|
345
|
+
spanId: traceContext?.spanId,
|
|
346
|
+
traceId: traceContext?.traceId,
|
|
347
|
+
traceSampled: traceContext?.traceSampled,
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const reqWithContext = req as express.Request & {
|
|
351
|
+
jobId?: string;
|
|
352
|
+
requestId?: string;
|
|
353
|
+
sessionId?: string;
|
|
354
|
+
};
|
|
355
|
+
if (jobId) {
|
|
356
|
+
reqWithContext.jobId = jobId;
|
|
357
|
+
}
|
|
358
|
+
reqWithContext.requestId = requestId;
|
|
359
|
+
if (sessionId) {
|
|
360
|
+
reqWithContext.sessionId = sessionId;
|
|
361
|
+
}
|
|
362
|
+
res.setHeader("X-Request-ID", requestId);
|
|
363
|
+
|
|
364
|
+
requestContextStorage.run(context, () => {
|
|
365
|
+
updateRequestContextFromRequest(req, res);
|
|
366
|
+
next();
|
|
367
|
+
});
|
|
368
|
+
};
|