@terreno/api 0.13.2 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/dist/__tests__/versionCheckPlugin.test.js +53 -3
  2. package/dist/api.arrayOperations.test.js +1 -0
  3. package/dist/api.asyncHandler.test.d.ts +1 -0
  4. package/dist/api.asyncHandler.test.js +236 -0
  5. package/dist/api.d.ts +15 -4
  6. package/dist/api.errors.test.js +1 -0
  7. package/dist/api.hooks.test.js +1 -0
  8. package/dist/api.js +153 -104
  9. package/dist/api.query.test.js +1 -0
  10. package/dist/api.test.js +174 -0
  11. package/dist/auth.d.ts +10 -5
  12. package/dist/auth.js +163 -90
  13. package/dist/auth.test.js +159 -0
  14. package/dist/betterAuthApp.test.js +1 -0
  15. package/dist/betterAuthSetup.d.ts +5 -6
  16. package/dist/betterAuthSetup.js +17 -14
  17. package/dist/betterAuthSetup.test.js +1 -0
  18. package/dist/config.d.ts +48 -0
  19. package/dist/config.js +248 -0
  20. package/dist/config.test.d.ts +1 -0
  21. package/dist/config.test.js +328 -0
  22. package/dist/configuration.test.js +1 -0
  23. package/dist/configurationApp.d.ts +1 -1
  24. package/dist/configurationApp.js +17 -13
  25. package/dist/configurationPlugin.test.js +1 -0
  26. package/dist/consentApp.test.js +1 -0
  27. package/dist/envConfigurationPlugin.d.ts +2 -0
  28. package/dist/envConfigurationPlugin.js +173 -0
  29. package/dist/envConfigurationPlugin.test.d.ts +1 -0
  30. package/dist/envConfigurationPlugin.test.js +322 -0
  31. package/dist/errors.d.ts +18 -7
  32. package/dist/errors.js +106 -10
  33. package/dist/errors.test.js +16 -1
  34. package/dist/example.js +16 -7
  35. package/dist/expressServer.d.ts +10 -9
  36. package/dist/expressServer.js +62 -53
  37. package/dist/expressServer.test.js +53 -2
  38. package/dist/githubAuth.d.ts +2 -1
  39. package/dist/githubAuth.js +41 -26
  40. package/dist/githubAuth.test.js +1 -0
  41. package/dist/index.d.ts +4 -0
  42. package/dist/index.js +4 -0
  43. package/dist/logger.d.ts +1 -1
  44. package/dist/logger.js +42 -20
  45. package/dist/models/versionConfig.d.ts +2 -0
  46. package/dist/models/versionConfig.js +8 -0
  47. package/dist/notifiers/googleChatNotifier.js +14 -16
  48. package/dist/notifiers/googleChatNotifier.test.js +1 -0
  49. package/dist/notifiers/slackNotifier.js +16 -14
  50. package/dist/notifiers/slackNotifier.test.js +41 -3
  51. package/dist/notifiers/zoomNotifier.js +7 -10
  52. package/dist/notifiers/zoomNotifier.test.js +1 -0
  53. package/dist/openApi.d.ts +1 -1
  54. package/dist/openApi.test.js +1 -0
  55. package/dist/openApiBuilder.d.ts +39 -6
  56. package/dist/openApiBuilder.js +1 -31
  57. package/dist/openApiBuilder.test.js +1 -0
  58. package/dist/openApiValidator.js +1 -0
  59. package/dist/openApiValidator.test.js +65 -0
  60. package/dist/permissions.d.ts +4 -4
  61. package/dist/permissions.js +67 -65
  62. package/dist/permissions.middleware.test.js +1 -0
  63. package/dist/permissions.test.js +1 -0
  64. package/dist/plugins.d.ts +5 -5
  65. package/dist/plugins.js +18 -9
  66. package/dist/plugins.test.js +1 -1
  67. package/dist/populate.d.ts +15 -8
  68. package/dist/populate.js +23 -24
  69. package/dist/populate.test.js +1 -0
  70. package/dist/realtime/changeStreamWatcher.d.ts +73 -0
  71. package/dist/realtime/changeStreamWatcher.js +720 -0
  72. package/dist/realtime/index.d.ts +6 -0
  73. package/dist/realtime/index.js +27 -0
  74. package/dist/realtime/queryMatcher.d.ts +14 -0
  75. package/dist/realtime/queryMatcher.js +250 -0
  76. package/dist/realtime/queryStore.d.ts +37 -0
  77. package/dist/realtime/queryStore.js +195 -0
  78. package/dist/realtime/realtime.test.d.ts +10 -0
  79. package/dist/realtime/realtime.test.js +2158 -0
  80. package/dist/realtime/realtimeApp.d.ts +93 -0
  81. package/dist/realtime/realtimeApp.js +560 -0
  82. package/dist/realtime/registry.d.ts +40 -0
  83. package/dist/realtime/registry.js +38 -0
  84. package/dist/realtime/socketUser.d.ts +10 -0
  85. package/dist/realtime/socketUser.js +17 -0
  86. package/dist/realtime/types.d.ts +100 -0
  87. package/dist/realtime/types.js +2 -0
  88. package/dist/requestContext.d.ts +37 -0
  89. package/dist/requestContext.js +344 -0
  90. package/dist/requestContext.test.d.ts +1 -0
  91. package/dist/requestContext.test.js +241 -0
  92. package/dist/terrenoApp.d.ts +8 -0
  93. package/dist/terrenoApp.js +50 -13
  94. package/dist/terrenoApp.test.js +194 -21
  95. package/dist/terrenoPlugin.d.ts +11 -0
  96. package/dist/tests/bunSetup.js +1 -0
  97. package/dist/tests.js +1 -1
  98. package/dist/transformers.d.ts +2 -2
  99. package/dist/transformers.js +5 -3
  100. package/dist/transformers.test.js +90 -0
  101. package/dist/types/consentResponse.d.ts +6 -3
  102. package/dist/versionCheckPlugin.d.ts +2 -0
  103. package/dist/versionCheckPlugin.js +18 -12
  104. package/package.json +4 -2
  105. package/src/__tests__/versionCheckPlugin.test.ts +37 -3
  106. package/src/api.arrayOperations.test.ts +1 -0
  107. package/src/api.asyncHandler.test.ts +177 -0
  108. package/src/api.errors.test.ts +1 -0
  109. package/src/api.hooks.test.ts +1 -0
  110. package/src/api.query.test.ts +1 -0
  111. package/src/api.test.ts +132 -0
  112. package/src/api.ts +199 -84
  113. package/src/auth.test.ts +160 -0
  114. package/src/auth.ts +120 -50
  115. package/src/betterAuthApp.test.ts +1 -0
  116. package/src/betterAuthSetup.test.ts +1 -0
  117. package/src/betterAuthSetup.ts +46 -19
  118. package/src/config.test.ts +255 -0
  119. package/src/config.ts +206 -0
  120. package/src/configuration.test.ts +1 -0
  121. package/src/configurationApp.ts +59 -24
  122. package/src/configurationPlugin.test.ts +1 -0
  123. package/src/consentApp.test.ts +1 -0
  124. package/src/envConfigurationPlugin.test.ts +143 -0
  125. package/src/envConfigurationPlugin.ts +100 -0
  126. package/src/errors.test.ts +19 -1
  127. package/src/errors.ts +94 -20
  128. package/src/example.ts +46 -21
  129. package/src/express.d.ts +18 -1
  130. package/src/expressServer.test.ts +50 -2
  131. package/src/expressServer.ts +80 -50
  132. package/src/githubAuth.test.ts +1 -0
  133. package/src/githubAuth.ts +59 -38
  134. package/src/index.ts +4 -0
  135. package/src/logger.ts +47 -17
  136. package/src/models/versionConfig.ts +13 -2
  137. package/src/notifiers/googleChatNotifier.test.ts +1 -0
  138. package/src/notifiers/googleChatNotifier.ts +7 -9
  139. package/src/notifiers/slackNotifier.test.ts +29 -3
  140. package/src/notifiers/slackNotifier.ts +9 -7
  141. package/src/notifiers/zoomNotifier.test.ts +1 -0
  142. package/src/notifiers/zoomNotifier.ts +8 -11
  143. package/src/openApi.test.ts +1 -0
  144. package/src/openApi.ts +4 -4
  145. package/src/openApiBuilder.test.ts +1 -0
  146. package/src/openApiBuilder.ts +14 -11
  147. package/src/openApiValidator.test.ts +59 -0
  148. package/src/openApiValidator.ts +3 -2
  149. package/src/permissions.middleware.test.ts +1 -0
  150. package/src/permissions.test.ts +1 -0
  151. package/src/permissions.ts +30 -25
  152. package/src/plugins.test.ts +1 -1
  153. package/src/plugins.ts +21 -14
  154. package/src/populate.test.ts +1 -0
  155. package/src/populate.ts +44 -36
  156. package/src/realtime/changeStreamWatcher.ts +568 -0
  157. package/src/realtime/index.ts +34 -0
  158. package/src/realtime/queryMatcher.ts +179 -0
  159. package/src/realtime/queryStore.ts +132 -0
  160. package/src/realtime/realtime.test.ts +1755 -0
  161. package/src/realtime/realtimeApp.ts +478 -0
  162. package/src/realtime/registry.ts +64 -0
  163. package/src/realtime/socketUser.ts +25 -0
  164. package/src/realtime/types.ts +112 -0
  165. package/src/requestContext.test.ts +196 -0
  166. package/src/requestContext.ts +368 -0
  167. package/src/terrenoApp.test.ts +137 -11
  168. package/src/terrenoApp.ts +64 -17
  169. package/src/terrenoPlugin.ts +12 -0
  170. package/src/tests/bunSetup.ts +1 -0
  171. package/src/tests.ts +7 -2
  172. package/src/transformers.test.ts +70 -2
  173. package/src/transformers.ts +15 -7
  174. package/src/types/consentResponse.ts +8 -10
  175. package/src/versionCheckPlugin.ts +15 -7
@@ -0,0 +1,196 @@
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("adds job id to logger context", () => {
168
+ let output = "";
169
+ const stream = new Writable({
170
+ write: (chunk, _encoding, callback): void => {
171
+ output += chunk.toString();
172
+ callback();
173
+ },
174
+ });
175
+
176
+ setupLogging({
177
+ disableConsoleLogging: true,
178
+ disableFileLogging: true,
179
+ transports: [
180
+ new winston.transports.Stream({
181
+ format: winston.format.printf((info) => {
182
+ return `${info.level}: ${info.message} requestId=${info.requestId} jobId=${info.jobId}`;
183
+ }),
184
+ stream,
185
+ }),
186
+ ],
187
+ });
188
+
189
+ runWithRequestContext({jobId: "job-log-1", requestId: "request-log-1"}, () => {
190
+ logger.info("worker handled job");
191
+ });
192
+
193
+ expect(output).toContain("requestId=request-log-1");
194
+ expect(output).toContain("jobId=job-log-1");
195
+ });
196
+ });
@@ -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
+ };