@replayio-app-building/netlify-recorder 0.15.10 → 0.16.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/dist/index.d.ts +48 -183
- package/dist/index.js +141 -329
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -93,66 +93,24 @@ interface BlobData {
|
|
|
93
93
|
handlerResponse?: HandlerResponse$1;
|
|
94
94
|
}
|
|
95
95
|
interface FinishRequestCallbacks {
|
|
96
|
-
/** Uploads serialized captured data and returns the blob URL. */
|
|
97
|
-
uploadBlob: (data: string) => Promise<string>;
|
|
98
96
|
/**
|
|
99
|
-
* Stores
|
|
97
|
+
* Stores the captured request data and returns the request ID.
|
|
100
98
|
*
|
|
101
99
|
* When `requestId` is provided (from `createRecordingRequestHandler`'s
|
|
102
100
|
* `waitUntil` flow), the callback should use it as the row ID so the
|
|
103
|
-
* client-facing header and the stored record match.
|
|
104
|
-
* callback generates its own ID
|
|
101
|
+
* client-facing header and the stored record match. When omitted, the
|
|
102
|
+
* callback generates its own ID.
|
|
105
103
|
*/
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
storeRequest: (data: {
|
|
105
|
+
blobData: string;
|
|
108
106
|
commitSha: string;
|
|
109
107
|
branchName: string;
|
|
110
108
|
repositoryUrl: string;
|
|
111
109
|
handlerPath: string;
|
|
112
110
|
/** Pre-generated request ID. Use as the row ID when provided. */
|
|
113
111
|
requestId?: string;
|
|
114
|
-
/** Optional secret that restricts access to this request. */
|
|
115
|
-
secret?: string;
|
|
116
112
|
}) => Promise<string>;
|
|
117
113
|
}
|
|
118
|
-
/**
|
|
119
|
-
* Infrastructure credentials required to start a recording container.
|
|
120
|
-
*
|
|
121
|
-
* The container is started on Fly.io via the `@replayio/app-building` package.
|
|
122
|
-
* It requires Infisical credentials (for secrets management inside the
|
|
123
|
-
* container) and a Fly.io token + app name.
|
|
124
|
-
*
|
|
125
|
-
* These must be set as environment variables on the Netlify site:
|
|
126
|
-
* INFISICAL_CLIENT_ID, INFISICAL_CLIENT_SECRET,
|
|
127
|
-
* INFISICAL_PROJECT_ID, INFISICAL_ENVIRONMENT,
|
|
128
|
-
* FLY_API_TOKEN, FLY_APP_NAME
|
|
129
|
-
*/
|
|
130
|
-
interface ContainerInfraConfig {
|
|
131
|
-
infisicalClientId: string;
|
|
132
|
-
infisicalClientSecret: string;
|
|
133
|
-
infisicalProjectId: string;
|
|
134
|
-
infisicalEnvironment: string;
|
|
135
|
-
flyToken: string;
|
|
136
|
-
flyApp: string;
|
|
137
|
-
}
|
|
138
|
-
interface EnsureRecordingOptions {
|
|
139
|
-
repositoryUrl: string;
|
|
140
|
-
/** Infrastructure credentials for starting the recording container. */
|
|
141
|
-
infraConfig?: ContainerInfraConfig;
|
|
142
|
-
/** Webhook URL the container can POST log entries to (optional). */
|
|
143
|
-
webhookUrl?: string;
|
|
144
|
-
/** Looks up request metadata by ID. */
|
|
145
|
-
lookupRequest: (requestId: string) => Promise<{
|
|
146
|
-
blobUrl: string;
|
|
147
|
-
commitSha: string;
|
|
148
|
-
branchName: string;
|
|
149
|
-
handlerPath: string;
|
|
150
|
-
}>;
|
|
151
|
-
/** Updates the request status (and optionally recording ID) in the database. */
|
|
152
|
-
updateStatus: (requestId: string, status: string, recordingId?: string) => Promise<void>;
|
|
153
|
-
/** Optional callback for the caller to emit structured log entries. */
|
|
154
|
-
onLog?: (level: "info" | "warn" | "error", message: string) => Promise<void>;
|
|
155
|
-
}
|
|
156
114
|
|
|
157
115
|
/**
|
|
158
116
|
* Called at the beginning of a Netlify handler execution.
|
|
@@ -212,32 +170,16 @@ interface FinishRequestOptions {
|
|
|
212
170
|
repositoryUrl?: string;
|
|
213
171
|
/**
|
|
214
172
|
* Pre-generated request ID. When provided, this ID is passed to the
|
|
215
|
-
* `
|
|
173
|
+
* `storeRequest` callback so the stored row matches the ID already
|
|
216
174
|
* sent to the client in the `X-Replay-Request-Id` header.
|
|
217
|
-
*
|
|
218
|
-
* Used by `createRecordingRequestHandler` in the `waitUntil` flow where
|
|
219
|
-
* the response is returned before `finishRequest` runs.
|
|
220
175
|
*/
|
|
221
176
|
requestId?: string;
|
|
222
|
-
/**
|
|
223
|
-
* Optional secret string. When set, the stored request is only
|
|
224
|
-
* accessible via API calls that provide the same secret value.
|
|
225
|
-
*/
|
|
226
|
-
secret?: string;
|
|
227
177
|
}
|
|
228
178
|
/**
|
|
229
179
|
* Called at the end of the handler execution.
|
|
230
180
|
* Restores original globals, serializes all captured data,
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
* **Important:** The returned response includes the `X-Replay-Request-Id`
|
|
235
|
-
* header. You must send the returned response to the client — not the
|
|
236
|
-
* original response object you passed in.
|
|
237
|
-
*
|
|
238
|
-
* Logs a warning to `console.warn` when the total finishRequest time or
|
|
239
|
-
* individual callback steps exceed their thresholds, to help diagnose
|
|
240
|
-
* slow blob uploads or database writes.
|
|
181
|
+
* stores the request via the provided callback, and sets the
|
|
182
|
+
* X-Replay-Request-Id header.
|
|
241
183
|
*/
|
|
242
184
|
declare function finishRequest(requestContext: RequestContext, callbacks: FinishRequestCallbacks, response: FullHandlerResponse, options?: FinishRequestOptions): Promise<FullHandlerResponse>;
|
|
243
185
|
|
|
@@ -254,79 +196,14 @@ interface CreateRecordingRequestHandlerOptions extends FinishRequestOptions {
|
|
|
254
196
|
*
|
|
255
197
|
* Automatically calls `startRequest` before the handler and `finishRequest`
|
|
256
198
|
* after, capturing all outbound network calls and environment variable reads.
|
|
257
|
-
*
|
|
199
|
+
* The captured data is stored via the provided callbacks.
|
|
258
200
|
*
|
|
259
201
|
* **Response timing:** When the Netlify Functions v2 `context` object is
|
|
260
202
|
* available (with `waitUntil`), the response is returned to the client
|
|
261
203
|
* **immediately** with a pre-generated `X-Replay-Request-Id` header. The
|
|
262
|
-
*
|
|
263
|
-
* `context.waitUntil()`. This avoids adding latency to the client response.
|
|
264
|
-
*
|
|
265
|
-
* When `context.waitUntil` is not available (v1 handlers or missing context),
|
|
266
|
-
* the wrapper falls back to awaiting `finishRequest` before returning.
|
|
267
|
-
*
|
|
268
|
-
* For v2 handlers the request body is read from a clone internally — your
|
|
269
|
-
* handler still receives the original request with an unconsumed body.
|
|
270
|
-
*
|
|
271
|
-
* @example v1 handler (NetlifyEvent)
|
|
272
|
-
* ```typescript
|
|
273
|
-
* import { createRecordingRequestHandler, remoteCallbacks } from "@replayio-app-building/netlify-recorder";
|
|
274
|
-
*
|
|
275
|
-
* const handler = createRecordingRequestHandler(
|
|
276
|
-
* async (event) => {
|
|
277
|
-
* const result = await myBusinessLogic(event.body);
|
|
278
|
-
* return {
|
|
279
|
-
* statusCode: 200,
|
|
280
|
-
* headers: { "Content-Type": "application/json" },
|
|
281
|
-
* body: JSON.stringify(result),
|
|
282
|
-
* };
|
|
283
|
-
* },
|
|
284
|
-
* {
|
|
285
|
-
* callbacks: remoteCallbacks("https://netlify-recorder-bm4wmw.netlify.app"),
|
|
286
|
-
* handlerPath: "netlify/functions/my-handler",
|
|
287
|
-
* }
|
|
288
|
-
* );
|
|
289
|
-
*
|
|
290
|
-
* export { handler };
|
|
291
|
-
* ```
|
|
292
|
-
*
|
|
293
|
-
* @example v2 handler (Web API Request)
|
|
294
|
-
* ```typescript
|
|
295
|
-
* import { createRecordingRequestHandler, remoteCallbacks } from "@replayio-app-building/netlify-recorder";
|
|
296
|
-
*
|
|
297
|
-
* export default createRecordingRequestHandler(
|
|
298
|
-
* async (req) => {
|
|
299
|
-
* // Body is still available — startRequest reads from a clone
|
|
300
|
-
* const body = await (req as Request).json();
|
|
301
|
-
* const result = await myBusinessLogic(body);
|
|
302
|
-
* return {
|
|
303
|
-
* statusCode: 200,
|
|
304
|
-
* headers: { "Content-Type": "application/json" },
|
|
305
|
-
* body: JSON.stringify(result),
|
|
306
|
-
* };
|
|
307
|
-
* },
|
|
308
|
-
* {
|
|
309
|
-
* callbacks: remoteCallbacks("https://netlify-recorder-bm4wmw.netlify.app"),
|
|
310
|
-
* handlerPath: "netlify/functions/my-handler",
|
|
311
|
-
* }
|
|
312
|
-
* );
|
|
313
|
-
* ```
|
|
314
|
-
*/
|
|
315
|
-
declare function createRecordingRequestHandler(handler: (event: NetlifyEvent | NetlifyV2Request, context?: unknown) => Promise<HandlerResponse>, options: CreateRecordingRequestHandlerOptions): (event: NetlifyEvent | NetlifyV2Request, context?: unknown) => Promise<HandlerResponse | Response>;
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Creates `FinishRequestCallbacks` that send captured data to a remote
|
|
319
|
-
* Netlify Recorder service. This removes the need for the consuming app
|
|
320
|
-
* to set up its own blob storage or database tables.
|
|
321
|
-
*
|
|
322
|
-
* The callbacks upload blob data and store request metadata via the
|
|
323
|
-
* service's `/api/store-request` endpoint, which handles UploadThing
|
|
324
|
-
* storage and database insertion.
|
|
325
|
-
*
|
|
326
|
-
* @param serviceUrl - Base URL of the Netlify Recorder service
|
|
327
|
-
* (e.g. "https://netlify-recorder-bm4wmw.netlify.app")
|
|
204
|
+
* data storage continues in the background via `context.waitUntil()`.
|
|
328
205
|
*/
|
|
329
|
-
declare function
|
|
206
|
+
declare function createRecordingRequestHandler(handler: (event: NetlifyEvent | NetlifyV2Request, context?: unknown) => Promise<HandlerResponse>, options: CreateRecordingRequestHandlerOptions): (event: NetlifyEvent | NetlifyV2Request, context?: unknown) => Promise<HandlerResponse>;
|
|
330
207
|
|
|
331
208
|
/**
|
|
332
209
|
* Redacts sensitive environment variable values from blob data.
|
|
@@ -348,28 +225,6 @@ declare function remoteCallbacks(serviceUrl: string): FinishRequestCallbacks;
|
|
|
348
225
|
*/
|
|
349
226
|
declare function redactBlobData(blobData: BlobData): BlobData;
|
|
350
227
|
|
|
351
|
-
/**
|
|
352
|
-
* Called by a background function to convert a request ID into a Replay recording ID.
|
|
353
|
-
*
|
|
354
|
-
* The function:
|
|
355
|
-
* 1. Looks up request metadata (blob URL, commit, handler path).
|
|
356
|
-
* 2. Delegates to `spawnRecordingContainer` which starts a detached Fly.io
|
|
357
|
-
* container, runs the recording script under replay-node, and uploads
|
|
358
|
-
* the resulting recording.
|
|
359
|
-
* 3. Updates the request status with the recording ID.
|
|
360
|
-
*
|
|
361
|
-
* **Required infrastructure:** Infisical credentials and a Fly.io token/app.
|
|
362
|
-
* See `ContainerInfraConfig` in types.ts for details. When these are not
|
|
363
|
-
* configured the function fails with an actionable error message listing
|
|
364
|
-
* the missing environment variables.
|
|
365
|
-
*/
|
|
366
|
-
declare function ensureRequestRecording(requestId: string, options: EnsureRecordingOptions): Promise<string>;
|
|
367
|
-
/**
|
|
368
|
-
* Reads infrastructure config from environment variables.
|
|
369
|
-
* Returns undefined if any required variable is missing.
|
|
370
|
-
*/
|
|
371
|
-
declare function readInfraConfigFromEnv(): ContainerInfraConfig | undefined;
|
|
372
|
-
|
|
373
228
|
interface RecordingResult {
|
|
374
229
|
/** Whether a response mismatch was detected between capture and replay. */
|
|
375
230
|
responseMismatch: boolean;
|
|
@@ -394,39 +249,49 @@ interface RecordingResult {
|
|
|
394
249
|
*/
|
|
395
250
|
declare function createRequestRecording(blobUrlOrData: string | BlobData, handlerPath: string, requestInfo: RequestInfo): Promise<RecordingResult>;
|
|
396
251
|
|
|
252
|
+
type SqlFunction$1 = (...args: any[]) => Promise<any[]>;
|
|
253
|
+
interface BackendRequest {
|
|
254
|
+
id: string;
|
|
255
|
+
blob_data: string;
|
|
256
|
+
handler_path: string;
|
|
257
|
+
commit_sha: string;
|
|
258
|
+
branch_name: string;
|
|
259
|
+
repository_url: string | null;
|
|
260
|
+
status: string;
|
|
261
|
+
recording_id: string | null;
|
|
262
|
+
error_message: string | null;
|
|
263
|
+
created_at: string;
|
|
264
|
+
updated_at: string;
|
|
265
|
+
}
|
|
397
266
|
/**
|
|
398
|
-
*
|
|
399
|
-
*
|
|
267
|
+
* Creates the `backend_requests` table. Call during schema initialization.
|
|
268
|
+
*
|
|
269
|
+
* Each package client stores captured request data in its own database
|
|
270
|
+
* using this table. The blob data (captured network calls, env reads, etc.)
|
|
271
|
+
* is stored directly in the `blob_data` column rather than in external
|
|
272
|
+
* blob storage.
|
|
400
273
|
*/
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
274
|
+
declare function backendRequestsEnsureTable(sql: SqlFunction$1): Promise<void>;
|
|
275
|
+
declare function backendRequestsInsert(sql: SqlFunction$1, data: {
|
|
276
|
+
id?: string;
|
|
277
|
+
blobData: string;
|
|
405
278
|
handlerPath: string;
|
|
406
|
-
/** Git commit SHA to check out inside the container. */
|
|
407
279
|
commitSha: string;
|
|
408
|
-
/** Git branch to clone. */
|
|
409
280
|
branchName: string;
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
281
|
+
repositoryUrl?: string | null;
|
|
282
|
+
}): Promise<string>;
|
|
283
|
+
declare function backendRequestsGet(sql: SqlFunction$1, id: string): Promise<BackendRequest | null>;
|
|
284
|
+
declare function backendRequestsGetBlobData(sql: SqlFunction$1, id: string): Promise<string | null>;
|
|
285
|
+
declare function backendRequestsList(sql: SqlFunction$1, filters?: {
|
|
286
|
+
status?: string;
|
|
287
|
+
limit?: number;
|
|
288
|
+
}): Promise<BackendRequest[]>;
|
|
289
|
+
declare function backendRequestsUpdateStatus(sql: SqlFunction$1, id: string, status: string, recordingId?: string, errorMessage?: string): Promise<void>;
|
|
419
290
|
/**
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
* 2. Checks out the exact commit
|
|
423
|
-
* 3. Runs `scripts/create-request-recording.ts` under replay-node
|
|
424
|
-
* 4. Uploads the resulting recording
|
|
425
|
-
* 5. Outputs the recording ID
|
|
426
|
-
*
|
|
427
|
-
* Returns the recording ID on success, or throws on failure.
|
|
291
|
+
* Convenience helper: creates `FinishRequestCallbacks` that store
|
|
292
|
+
* captured request data directly in the `backend_requests` table.
|
|
428
293
|
*/
|
|
429
|
-
declare function
|
|
294
|
+
declare function databaseCallbacks(sql: SqlFunction$1): FinishRequestCallbacks;
|
|
430
295
|
|
|
431
296
|
type SqlFunction = (...args: any[]) => Promise<any[]>;
|
|
432
297
|
/**
|
|
@@ -451,4 +316,4 @@ declare function databaseAuditDumpLogTable(sql: SqlFunction): Promise<Record<str
|
|
|
451
316
|
|
|
452
317
|
declare function getCurrentRequestId(): string | null;
|
|
453
318
|
|
|
454
|
-
export { type
|
|
319
|
+
export { type BackendRequest, type BlobData, type CapturedData, type CreateRecordingRequestHandlerOptions, type EnvRead, type FinishRequestCallbacks, type FinishRequestOptions, type HandlerResponse$1 as HandlerResponse, type NetlifyEvent, type NetlifyV2Request, type NetworkCall, type RecordingResult, type RequestContext, type RequestInfo, backendRequestsEnsureTable, backendRequestsGet, backendRequestsGetBlobData, backendRequestsInsert, backendRequestsList, backendRequestsUpdateStatus, createRecordingRequestHandler, createRequestRecording, databaseAuditDumpLogTable, databaseAuditEnsureLogTable, databaseAuditMonitorTable, databaseCallbacks, finishRequest, getCurrentRequestId, redactBlobData, startRequest };
|
package/dist/index.js
CHANGED
|
@@ -490,7 +490,6 @@ function redactBlobData(blobData) {
|
|
|
490
490
|
|
|
491
491
|
// src/finishRequest.ts
|
|
492
492
|
var SLOW_THRESHOLD_MS = 2e3;
|
|
493
|
-
var SLOW_STEP_THRESHOLD_MS = 1e3;
|
|
494
493
|
async function finishRequest(requestContext, callbacks, response, options) {
|
|
495
494
|
const finishStart = Date.now();
|
|
496
495
|
requestContext.cleanup();
|
|
@@ -535,34 +534,20 @@ async function finishRequest(requestContext, callbacks, response, options) {
|
|
|
535
534
|
};
|
|
536
535
|
const blobData = redactBlobData(rawBlobData);
|
|
537
536
|
const blobContent = JSON.stringify(blobData);
|
|
538
|
-
const uploadStart = Date.now();
|
|
539
|
-
const blobUrl = await callbacks.uploadBlob(blobContent);
|
|
540
|
-
const uploadDuration = Date.now() - uploadStart;
|
|
541
|
-
if (uploadDuration > SLOW_STEP_THRESHOLD_MS) {
|
|
542
|
-
console.warn(
|
|
543
|
-
`netlify-recorder: uploadBlob took ${uploadDuration}ms (handler: ${handlerPath})`
|
|
544
|
-
);
|
|
545
|
-
}
|
|
546
537
|
const storeStart = Date.now();
|
|
547
|
-
const storedRequestId = await callbacks.
|
|
548
|
-
|
|
538
|
+
const storedRequestId = await callbacks.storeRequest({
|
|
539
|
+
blobData: blobContent,
|
|
549
540
|
commitSha,
|
|
550
541
|
branchName,
|
|
551
542
|
repositoryUrl,
|
|
552
543
|
handlerPath,
|
|
553
|
-
requestId: options?.requestId
|
|
554
|
-
secret: options?.secret
|
|
544
|
+
requestId: options?.requestId
|
|
555
545
|
});
|
|
556
546
|
const storeDuration = Date.now() - storeStart;
|
|
557
|
-
if (storeDuration > SLOW_STEP_THRESHOLD_MS) {
|
|
558
|
-
console.warn(
|
|
559
|
-
`netlify-recorder: storeRequestData took ${storeDuration}ms (handler: ${handlerPath})`
|
|
560
|
-
);
|
|
561
|
-
}
|
|
562
547
|
const totalDuration = Date.now() - finishStart;
|
|
563
548
|
if (totalDuration > SLOW_THRESHOLD_MS) {
|
|
564
549
|
console.warn(
|
|
565
|
-
`netlify-recorder: finishRequest took ${totalDuration}ms total (
|
|
550
|
+
`netlify-recorder: finishRequest took ${totalDuration}ms total (store: ${storeDuration}ms, handler: ${handlerPath})`
|
|
566
551
|
);
|
|
567
552
|
}
|
|
568
553
|
return {
|
|
@@ -578,7 +563,6 @@ async function finishRequest(requestContext, callbacks, response, options) {
|
|
|
578
563
|
import crypto from "crypto";
|
|
579
564
|
function createRecordingRequestHandler(handler, options) {
|
|
580
565
|
return async (event, context) => {
|
|
581
|
-
const isV2 = isWebApiRequest(event);
|
|
582
566
|
const requestId = crypto.randomUUID();
|
|
583
567
|
setCurrentRequestId(requestId);
|
|
584
568
|
const reqContext = startRequest(event);
|
|
@@ -612,294 +596,10 @@ function createRecordingRequestHandler(handler, options) {
|
|
|
612
596
|
}
|
|
613
597
|
)
|
|
614
598
|
);
|
|
615
|
-
return
|
|
599
|
+
return responseWithHeader;
|
|
616
600
|
}
|
|
617
601
|
await finishRequest(reqContext, options.callbacks, response, finishOpts);
|
|
618
|
-
return
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
function toWebResponse(result) {
|
|
622
|
-
return new Response(result.body, {
|
|
623
|
-
status: result.statusCode,
|
|
624
|
-
headers: result.headers
|
|
625
|
-
});
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// src/remoteCallbacks.ts
|
|
629
|
-
function remoteCallbacks(serviceUrl) {
|
|
630
|
-
const base = serviceUrl.replace(/\/+$/, "");
|
|
631
|
-
let pendingBlobData;
|
|
632
|
-
return {
|
|
633
|
-
uploadBlob: async (data) => {
|
|
634
|
-
pendingBlobData = data;
|
|
635
|
-
return "__pending__";
|
|
636
|
-
},
|
|
637
|
-
storeRequestData: async (metadata) => {
|
|
638
|
-
const blobData = pendingBlobData;
|
|
639
|
-
pendingBlobData = void 0;
|
|
640
|
-
if (!blobData) {
|
|
641
|
-
throw new Error(
|
|
642
|
-
"remoteCallbacks: uploadBlob must be called before storeRequestData"
|
|
643
|
-
);
|
|
644
|
-
}
|
|
645
|
-
const res = await fetch(
|
|
646
|
-
`${base}/api/store-request`,
|
|
647
|
-
{
|
|
648
|
-
method: "POST",
|
|
649
|
-
headers: { "Content-Type": "application/json" },
|
|
650
|
-
body: JSON.stringify({
|
|
651
|
-
blobData,
|
|
652
|
-
handlerPath: metadata.handlerPath,
|
|
653
|
-
commitSha: metadata.commitSha,
|
|
654
|
-
branchName: metadata.branchName,
|
|
655
|
-
repositoryUrl: metadata.repositoryUrl,
|
|
656
|
-
requestId: metadata.requestId,
|
|
657
|
-
secret: metadata.secret
|
|
658
|
-
})
|
|
659
|
-
}
|
|
660
|
-
);
|
|
661
|
-
if (!res.ok) {
|
|
662
|
-
const errBody = await res.text().catch(() => "(unreadable)");
|
|
663
|
-
throw new Error(
|
|
664
|
-
`Netlify Recorder store-request failed: ${res.status} ${errBody}`
|
|
665
|
-
);
|
|
666
|
-
}
|
|
667
|
-
const result = await res.json();
|
|
668
|
-
return result.requestId;
|
|
669
|
-
}
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
// src/spawnRecordingContainer.ts
|
|
674
|
-
async function spawnRecordingContainer(options) {
|
|
675
|
-
const {
|
|
676
|
-
blobUrl,
|
|
677
|
-
handlerPath,
|
|
678
|
-
commitSha,
|
|
679
|
-
branchName,
|
|
680
|
-
repositoryUrl,
|
|
681
|
-
infraConfig,
|
|
682
|
-
logWebhookUrl,
|
|
683
|
-
onLog
|
|
684
|
-
} = options;
|
|
685
|
-
const emit = async (level, message) => {
|
|
686
|
-
if (onLog) {
|
|
687
|
-
try {
|
|
688
|
-
await onLog(level, message);
|
|
689
|
-
} catch {
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
};
|
|
693
|
-
await emit("info", "Logging in to Infisical");
|
|
694
|
-
const {
|
|
695
|
-
infisicalLogin,
|
|
696
|
-
startContainer,
|
|
697
|
-
FileContainerRegistry,
|
|
698
|
-
httpGet,
|
|
699
|
-
httpOptsFor
|
|
700
|
-
} = await import("@replayio/app-building");
|
|
701
|
-
const infisicalToken = await infisicalLogin(
|
|
702
|
-
infraConfig.infisicalClientId,
|
|
703
|
-
infraConfig.infisicalClientSecret
|
|
704
|
-
);
|
|
705
|
-
const infisicalConfig = {
|
|
706
|
-
token: infisicalToken,
|
|
707
|
-
projectId: infraConfig.infisicalProjectId,
|
|
708
|
-
environment: infraConfig.infisicalEnvironment
|
|
709
|
-
};
|
|
710
|
-
const registry = new FileContainerRegistry("/tmp/netlify-recorder-containers.json");
|
|
711
|
-
const initialPrompt = [
|
|
712
|
-
`IMPORTANT: Follow these steps EXACTLY. Run each command as shown. Print ALL output.`,
|
|
713
|
-
`Do NOT explore the codebase, read AGENTS.md, or deviate from these steps.`,
|
|
714
|
-
`Do NOT attempt to debug, fix, shim, or work around ANY errors. If a command fails,`,
|
|
715
|
-
`print the full error output and move on to the next step. Errors during handler replay`,
|
|
716
|
-
`(like "No more recorded network calls" or DB errors) are EXPECTED and harmless.`,
|
|
717
|
-
``,
|
|
718
|
-
`=== Step 1: Install dependencies ===`,
|
|
719
|
-
`cd /repo/apps/netlify-recorder && npm install 2>&1`,
|
|
720
|
-
``,
|
|
721
|
-
`=== Step 2: Checkout the exact commit ===`,
|
|
722
|
-
`git fetch origin ${commitSha} 2>&1 || git fetch --all 2>&1`,
|
|
723
|
-
`git checkout ${commitSha} 2>&1`,
|
|
724
|
-
``,
|
|
725
|
-
`=== Step 3: Verify recording script exists ===`,
|
|
726
|
-
`ls -la /repo/apps/netlify-recorder/scripts/create-request-recording.ts`,
|
|
727
|
-
`If the file does NOT exist, print "ERROR: create-request-recording.ts not found" and STOP.`,
|
|
728
|
-
``,
|
|
729
|
-
`=== Step 4: Pre-compile for replay-node (Node v16) ===`,
|
|
730
|
-
`replay-node is Node v16 \u2014 it cannot run TypeScript or use modern APIs directly.`,
|
|
731
|
-
`You MUST compile everything with esbuild first. Run these commands exactly:`,
|
|
732
|
-
``,
|
|
733
|
-
`# Install undici for web API polyfills (fetch, Headers, Response):`,
|
|
734
|
-
`cd /repo/apps/netlify-recorder && npm install undici@5 2>&1`,
|
|
735
|
-
``,
|
|
736
|
-
`# Create the polyfill loader:`,
|
|
737
|
-
`cat > /tmp/web-polyfill.cjs << 'POLYFILL'`,
|
|
738
|
-
`try {`,
|
|
739
|
-
` var u = require("undici");`,
|
|
740
|
-
` if (!globalThis.fetch) globalThis.fetch = u.fetch;`,
|
|
741
|
-
` if (!globalThis.Headers) globalThis.Headers = u.Headers;`,
|
|
742
|
-
` if (!globalThis.Response) globalThis.Response = u.Response;`,
|
|
743
|
-
` if (!globalThis.Request) globalThis.Request = u.Request;`,
|
|
744
|
-
`} catch(e) { console.error("polyfill warning:", e.message); }`,
|
|
745
|
-
`POLYFILL`,
|
|
746
|
-
``,
|
|
747
|
-
`# Compile the recording script (bundles all local TS dependencies):`,
|
|
748
|
-
`npx esbuild scripts/create-request-recording.ts \\`,
|
|
749
|
-
` --bundle --platform=node --target=node16 --format=cjs \\`,
|
|
750
|
-
` --outfile=/tmp/create-recording.cjs 2>&1`,
|
|
751
|
-
``,
|
|
752
|
-
`# Compile the handler (bundles everything including node_modules):`,
|
|
753
|
-
`npx esbuild ${handlerPath}.ts \\`,
|
|
754
|
-
` --bundle --platform=node --target=node16 --format=cjs \\`,
|
|
755
|
-
` --outfile=/tmp/handler.cjs 2>&1`,
|
|
756
|
-
``,
|
|
757
|
-
`=== Step 5: Run under replay-node ===`,
|
|
758
|
-
`This MUST use replay-node so the execution is recorded. Run exactly:`,
|
|
759
|
-
``,
|
|
760
|
-
`cd /repo/apps/netlify-recorder && npx @replayio/node \\`,
|
|
761
|
-
` -r /tmp/web-polyfill.cjs /tmp/create-recording.cjs \\`,
|
|
762
|
-
` --blob-url '${blobUrl}' \\`,
|
|
763
|
-
` --handler-path '/tmp/handler.cjs' 2>&1`,
|
|
764
|
-
``,
|
|
765
|
-
`The output will show captured data being replayed. Errors like "No more recorded`,
|
|
766
|
-
`network calls" or "DATABASE_URL" errors are EXPECTED \u2014 they come from post-handler`,
|
|
767
|
-
`DB operations that were not in the original blob. Do NOT try to fix these.`,
|
|
768
|
-
``,
|
|
769
|
-
`=== Step 6: Upload the recording ===`,
|
|
770
|
-
`exec-secrets RECORD_REPLAY_API_KEY -- npx replayio upload --all 2>&1`,
|
|
771
|
-
``,
|
|
772
|
-
`Find the recording ID (UUID) in the upload output and print:`,
|
|
773
|
-
` recording: <recording-id>`,
|
|
774
|
-
``,
|
|
775
|
-
`Then output <DONE>.`
|
|
776
|
-
].join("\n");
|
|
777
|
-
await emit("info", "Starting detached container on Fly.io");
|
|
778
|
-
const state = await startContainer(
|
|
779
|
-
{
|
|
780
|
-
infisical: infisicalConfig,
|
|
781
|
-
registry,
|
|
782
|
-
flyToken: infraConfig.flyToken,
|
|
783
|
-
flyApp: infraConfig.flyApp,
|
|
784
|
-
detached: true,
|
|
785
|
-
initialPrompt,
|
|
786
|
-
webhookUrl: logWebhookUrl
|
|
787
|
-
},
|
|
788
|
-
{
|
|
789
|
-
repoUrl: repositoryUrl,
|
|
790
|
-
cloneBranch: branchName
|
|
791
|
-
}
|
|
792
|
-
);
|
|
793
|
-
await emit("info", `Container started: ${state.containerName} at ${state.baseUrl}`);
|
|
794
|
-
const maxWaitMs = 10 * 60 * 1e3;
|
|
795
|
-
const pollIntervalMs = 1e4;
|
|
796
|
-
const deadline = Date.now() + maxWaitMs;
|
|
797
|
-
let containerDone = false;
|
|
798
|
-
while (Date.now() < deadline) {
|
|
799
|
-
try {
|
|
800
|
-
const status = await httpGet(`${state.baseUrl}/status`, httpOptsFor(state));
|
|
801
|
-
if (status?.state === "stopped" || status?.state === "stopping") {
|
|
802
|
-
containerDone = true;
|
|
803
|
-
break;
|
|
804
|
-
}
|
|
805
|
-
} catch {
|
|
806
|
-
containerDone = true;
|
|
807
|
-
break;
|
|
808
|
-
}
|
|
809
|
-
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
810
|
-
}
|
|
811
|
-
if (!containerDone) {
|
|
812
|
-
await emit("warn", "Container did not finish within 10 minutes");
|
|
813
|
-
}
|
|
814
|
-
let recordingId = null;
|
|
815
|
-
try {
|
|
816
|
-
const logs = await httpGet(`${state.baseUrl}/logs?offset=0`, httpOptsFor(state));
|
|
817
|
-
if (typeof logs === "string") {
|
|
818
|
-
const match = logs.match(/recording[:\s]+([a-f0-9-]{36})/i);
|
|
819
|
-
if (match?.[1]) {
|
|
820
|
-
recordingId = match[1];
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
} catch {
|
|
824
|
-
await emit("warn", "Could not read container logs after exit");
|
|
825
|
-
}
|
|
826
|
-
if (!recordingId) {
|
|
827
|
-
await emit("error", "Container completed but no recording ID was found in output");
|
|
828
|
-
throw new Error("Recording creation failed: no recording ID returned from container");
|
|
829
|
-
}
|
|
830
|
-
await emit("info", `Container completed \u2014 recording ID: ${recordingId}`);
|
|
831
|
-
return recordingId;
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
// src/ensureRequestRecording.ts
|
|
835
|
-
async function ensureRequestRecording(requestId, options) {
|
|
836
|
-
const { repositoryUrl, infraConfig, webhookUrl, lookupRequest, updateStatus, onLog } = options;
|
|
837
|
-
const emit = async (level, message) => {
|
|
838
|
-
if (onLog) {
|
|
839
|
-
try {
|
|
840
|
-
await onLog(level, message);
|
|
841
|
-
} catch {
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
};
|
|
845
|
-
await updateStatus(requestId, "processing");
|
|
846
|
-
try {
|
|
847
|
-
if (!infraConfig) {
|
|
848
|
-
const missing = getMissingInfraVars();
|
|
849
|
-
throw new Error(
|
|
850
|
-
`Container infrastructure not configured. Missing environment variables: ${missing.join(", ")}. These must be set on the Netlify site for recording creation to work.`
|
|
851
|
-
);
|
|
852
|
-
}
|
|
853
|
-
const requestData = await lookupRequest(requestId);
|
|
854
|
-
await emit("info", `Request data retrieved \u2014 handler: ${requestData.handlerPath}, branch: ${requestData.branchName}, commit: ${requestData.commitSha}`);
|
|
855
|
-
const recordingId = await spawnRecordingContainer({
|
|
856
|
-
blobUrl: requestData.blobUrl,
|
|
857
|
-
handlerPath: requestData.handlerPath,
|
|
858
|
-
commitSha: requestData.commitSha,
|
|
859
|
-
branchName: requestData.branchName,
|
|
860
|
-
repositoryUrl,
|
|
861
|
-
infraConfig,
|
|
862
|
-
logWebhookUrl: webhookUrl,
|
|
863
|
-
onLog
|
|
864
|
-
});
|
|
865
|
-
await emit("info", `Recording created successfully: ${recordingId}`);
|
|
866
|
-
await updateStatus(requestId, "recorded", recordingId);
|
|
867
|
-
return recordingId;
|
|
868
|
-
} catch (err) {
|
|
869
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
870
|
-
await emit("error", `Recording creation failed: ${message}`);
|
|
871
|
-
await updateStatus(requestId, "failed");
|
|
872
|
-
throw err;
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
function getMissingInfraVars() {
|
|
876
|
-
const required = [
|
|
877
|
-
"INFISICAL_CLIENT_ID",
|
|
878
|
-
"INFISICAL_CLIENT_SECRET",
|
|
879
|
-
"INFISICAL_PROJECT_ID",
|
|
880
|
-
"INFISICAL_ENVIRONMENT",
|
|
881
|
-
"FLY_API_TOKEN",
|
|
882
|
-
"FLY_APP_NAME"
|
|
883
|
-
];
|
|
884
|
-
return required.filter((name) => !process.env[name]);
|
|
885
|
-
}
|
|
886
|
-
function readInfraConfigFromEnv() {
|
|
887
|
-
const clientId = process.env.INFISICAL_CLIENT_ID;
|
|
888
|
-
const clientSecret = process.env.INFISICAL_CLIENT_SECRET;
|
|
889
|
-
const projectId = process.env.INFISICAL_PROJECT_ID;
|
|
890
|
-
const environment = process.env.INFISICAL_ENVIRONMENT;
|
|
891
|
-
const flyToken = process.env.FLY_API_TOKEN;
|
|
892
|
-
const flyApp = process.env.FLY_APP_NAME;
|
|
893
|
-
if (!clientId || !clientSecret || !projectId || !environment || !flyToken || !flyApp) {
|
|
894
|
-
return void 0;
|
|
895
|
-
}
|
|
896
|
-
return {
|
|
897
|
-
infisicalClientId: clientId,
|
|
898
|
-
infisicalClientSecret: clientSecret,
|
|
899
|
-
infisicalProjectId: projectId,
|
|
900
|
-
infisicalEnvironment: environment,
|
|
901
|
-
flyToken,
|
|
902
|
-
flyApp
|
|
602
|
+
return responseWithHeader;
|
|
903
603
|
};
|
|
904
604
|
}
|
|
905
605
|
|
|
@@ -1210,6 +910,131 @@ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo) {
|
|
|
1210
910
|
return result;
|
|
1211
911
|
}
|
|
1212
912
|
|
|
913
|
+
// src/backendRequests.ts
|
|
914
|
+
async function backendRequestsEnsureTable(sql) {
|
|
915
|
+
await sql`
|
|
916
|
+
CREATE TABLE IF NOT EXISTS backend_requests (
|
|
917
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
918
|
+
blob_data TEXT NOT NULL,
|
|
919
|
+
handler_path TEXT NOT NULL,
|
|
920
|
+
commit_sha TEXT NOT NULL,
|
|
921
|
+
branch_name TEXT NOT NULL DEFAULT 'main',
|
|
922
|
+
repository_url TEXT,
|
|
923
|
+
status TEXT NOT NULL DEFAULT 'captured'
|
|
924
|
+
CHECK (status IN ('captured', 'queued', 'processing', 'recorded', 'failed')),
|
|
925
|
+
recording_id TEXT,
|
|
926
|
+
error_message TEXT,
|
|
927
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
928
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
929
|
+
)
|
|
930
|
+
`;
|
|
931
|
+
await sql`
|
|
932
|
+
CREATE INDEX IF NOT EXISTS idx_backend_requests_status ON backend_requests (status)
|
|
933
|
+
`;
|
|
934
|
+
await sql`
|
|
935
|
+
CREATE INDEX IF NOT EXISTS idx_backend_requests_created_at ON backend_requests (created_at DESC)
|
|
936
|
+
`;
|
|
937
|
+
}
|
|
938
|
+
async function backendRequestsInsert(sql, data) {
|
|
939
|
+
if (data.id) {
|
|
940
|
+
await sql`
|
|
941
|
+
INSERT INTO backend_requests (id, blob_data, handler_path, commit_sha, branch_name, repository_url)
|
|
942
|
+
VALUES (
|
|
943
|
+
${data.id}::uuid,
|
|
944
|
+
${data.blobData},
|
|
945
|
+
${data.handlerPath},
|
|
946
|
+
${data.commitSha},
|
|
947
|
+
${data.branchName},
|
|
948
|
+
${data.repositoryUrl ?? null}
|
|
949
|
+
)
|
|
950
|
+
`;
|
|
951
|
+
return data.id;
|
|
952
|
+
}
|
|
953
|
+
const rows = await sql`
|
|
954
|
+
INSERT INTO backend_requests (blob_data, handler_path, commit_sha, branch_name, repository_url)
|
|
955
|
+
VALUES (
|
|
956
|
+
${data.blobData},
|
|
957
|
+
${data.handlerPath},
|
|
958
|
+
${data.commitSha},
|
|
959
|
+
${data.branchName},
|
|
960
|
+
${data.repositoryUrl ?? null}
|
|
961
|
+
)
|
|
962
|
+
RETURNING id
|
|
963
|
+
`;
|
|
964
|
+
return rows[0]?.id ?? "";
|
|
965
|
+
}
|
|
966
|
+
async function backendRequestsGet(sql, id) {
|
|
967
|
+
const rows = await sql`
|
|
968
|
+
SELECT id, blob_data, handler_path, commit_sha, branch_name, repository_url,
|
|
969
|
+
status, recording_id, error_message, created_at, updated_at
|
|
970
|
+
FROM backend_requests WHERE id = ${id}
|
|
971
|
+
`;
|
|
972
|
+
return rows[0] ?? null;
|
|
973
|
+
}
|
|
974
|
+
async function backendRequestsGetBlobData(sql, id) {
|
|
975
|
+
const rows = await sql`
|
|
976
|
+
SELECT blob_data FROM backend_requests WHERE id = ${id}
|
|
977
|
+
`;
|
|
978
|
+
return rows[0]?.blob_data ?? null;
|
|
979
|
+
}
|
|
980
|
+
async function backendRequestsList(sql, filters) {
|
|
981
|
+
const limit = filters?.limit ?? 50;
|
|
982
|
+
if (filters?.status) {
|
|
983
|
+
const rows2 = await sql`
|
|
984
|
+
SELECT id, blob_data, handler_path, commit_sha, branch_name, repository_url,
|
|
985
|
+
status, recording_id, error_message, created_at, updated_at
|
|
986
|
+
FROM backend_requests
|
|
987
|
+
WHERE status = ${filters.status}
|
|
988
|
+
ORDER BY created_at DESC
|
|
989
|
+
LIMIT ${limit}
|
|
990
|
+
`;
|
|
991
|
+
return rows2;
|
|
992
|
+
}
|
|
993
|
+
const rows = await sql`
|
|
994
|
+
SELECT id, blob_data, handler_path, commit_sha, branch_name, repository_url,
|
|
995
|
+
status, recording_id, error_message, created_at, updated_at
|
|
996
|
+
FROM backend_requests
|
|
997
|
+
ORDER BY created_at DESC
|
|
998
|
+
LIMIT ${limit}
|
|
999
|
+
`;
|
|
1000
|
+
return rows;
|
|
1001
|
+
}
|
|
1002
|
+
async function backendRequestsUpdateStatus(sql, id, status, recordingId, errorMessage) {
|
|
1003
|
+
if (recordingId) {
|
|
1004
|
+
await sql`
|
|
1005
|
+
UPDATE backend_requests
|
|
1006
|
+
SET status = ${status}, recording_id = ${recordingId}, updated_at = NOW()
|
|
1007
|
+
WHERE id = ${id}
|
|
1008
|
+
`;
|
|
1009
|
+
} else if (errorMessage) {
|
|
1010
|
+
await sql`
|
|
1011
|
+
UPDATE backend_requests
|
|
1012
|
+
SET status = ${status}, error_message = ${errorMessage}, updated_at = NOW()
|
|
1013
|
+
WHERE id = ${id}
|
|
1014
|
+
`;
|
|
1015
|
+
} else {
|
|
1016
|
+
await sql`
|
|
1017
|
+
UPDATE backend_requests
|
|
1018
|
+
SET status = ${status}, updated_at = NOW()
|
|
1019
|
+
WHERE id = ${id}
|
|
1020
|
+
`;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
function databaseCallbacks(sql) {
|
|
1024
|
+
return {
|
|
1025
|
+
storeRequest: async (data) => {
|
|
1026
|
+
return backendRequestsInsert(sql, {
|
|
1027
|
+
id: data.requestId,
|
|
1028
|
+
blobData: data.blobData,
|
|
1029
|
+
handlerPath: data.handlerPath,
|
|
1030
|
+
commitSha: data.commitSha,
|
|
1031
|
+
branchName: data.branchName,
|
|
1032
|
+
repositoryUrl: data.repositoryUrl
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1213
1038
|
// src/databaseAudit.ts
|
|
1214
1039
|
async function databaseAuditEnsureLogTable(sql) {
|
|
1215
1040
|
await sql`
|
|
@@ -1236,23 +1061,7 @@ async function databaseAuditEnsureLogTable(sql) {
|
|
|
1236
1061
|
changed_cols TEXT[];
|
|
1237
1062
|
req_id TEXT;
|
|
1238
1063
|
call_idx INTEGER;
|
|
1239
|
-
pk_col TEXT;
|
|
1240
|
-
pk_val TEXT;
|
|
1241
1064
|
BEGIN
|
|
1242
|
-
-- Dynamically look up the primary key column for this table
|
|
1243
|
-
SELECT a.attname INTO pk_col
|
|
1244
|
-
FROM pg_index i
|
|
1245
|
-
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
|
1246
|
-
WHERE i.indrelid = TG_RELID AND i.indisprimary
|
|
1247
|
-
LIMIT 1;
|
|
1248
|
-
|
|
1249
|
-
-- Extract PK value from the appropriate record
|
|
1250
|
-
IF TG_OP = 'DELETE' THEN
|
|
1251
|
-
EXECUTE format('SELECT ($1.%I)::TEXT', pk_col) INTO pk_val USING OLD;
|
|
1252
|
-
ELSE
|
|
1253
|
-
EXECUTE format('SELECT ($1.%I)::TEXT', pk_col) INTO pk_val USING NEW;
|
|
1254
|
-
END IF;
|
|
1255
|
-
|
|
1256
1065
|
-- Read application context injected by the network interceptor
|
|
1257
1066
|
req_id := COALESCE(current_setting('app.replay_request_id', true), '');
|
|
1258
1067
|
IF req_id = '' THEN req_id := NULL; END IF;
|
|
@@ -1265,7 +1074,7 @@ async function databaseAuditEnsureLogTable(sql) {
|
|
|
1265
1074
|
|
|
1266
1075
|
IF TG_OP = 'INSERT' THEN
|
|
1267
1076
|
INSERT INTO audit_log (table_name, record_id, action, new_data, replay_request_id, replay_request_call_index)
|
|
1268
|
-
VALUES (TG_TABLE_NAME,
|
|
1077
|
+
VALUES (TG_TABLE_NAME, NEW.id::TEXT, 'INSERT', to_jsonb(NEW), req_id, call_idx);
|
|
1269
1078
|
RETURN NEW;
|
|
1270
1079
|
ELSIF TG_OP = 'UPDATE' THEN
|
|
1271
1080
|
SELECT ARRAY_AGG(n.key) INTO changed_cols
|
|
@@ -1274,11 +1083,11 @@ async function databaseAuditEnsureLogTable(sql) {
|
|
|
1274
1083
|
WHERE o.value IS DISTINCT FROM n.value;
|
|
1275
1084
|
|
|
1276
1085
|
INSERT INTO audit_log (table_name, record_id, action, old_data, new_data, changed_fields, replay_request_id, replay_request_call_index)
|
|
1277
|
-
VALUES (TG_TABLE_NAME,
|
|
1086
|
+
VALUES (TG_TABLE_NAME, NEW.id::TEXT, 'UPDATE', to_jsonb(OLD), to_jsonb(NEW), COALESCE(changed_cols, ARRAY[]::TEXT[]), req_id, call_idx);
|
|
1278
1087
|
RETURN NEW;
|
|
1279
1088
|
ELSIF TG_OP = 'DELETE' THEN
|
|
1280
1089
|
INSERT INTO audit_log (table_name, record_id, action, old_data, replay_request_id, replay_request_call_index)
|
|
1281
|
-
VALUES (TG_TABLE_NAME,
|
|
1090
|
+
VALUES (TG_TABLE_NAME, OLD.id::TEXT, 'DELETE', to_jsonb(OLD), req_id, call_idx);
|
|
1282
1091
|
RETURN OLD;
|
|
1283
1092
|
END IF;
|
|
1284
1093
|
RETURN NULL;
|
|
@@ -1306,17 +1115,20 @@ async function databaseAuditDumpLogTable(sql) {
|
|
|
1306
1115
|
return rows;
|
|
1307
1116
|
}
|
|
1308
1117
|
export {
|
|
1118
|
+
backendRequestsEnsureTable,
|
|
1119
|
+
backendRequestsGet,
|
|
1120
|
+
backendRequestsGetBlobData,
|
|
1121
|
+
backendRequestsInsert,
|
|
1122
|
+
backendRequestsList,
|
|
1123
|
+
backendRequestsUpdateStatus,
|
|
1309
1124
|
createRecordingRequestHandler,
|
|
1310
1125
|
createRequestRecording,
|
|
1311
1126
|
databaseAuditDumpLogTable,
|
|
1312
1127
|
databaseAuditEnsureLogTable,
|
|
1313
1128
|
databaseAuditMonitorTable,
|
|
1314
|
-
|
|
1129
|
+
databaseCallbacks,
|
|
1315
1130
|
finishRequest,
|
|
1316
1131
|
getCurrentRequestId,
|
|
1317
|
-
readInfraConfigFromEnv,
|
|
1318
1132
|
redactBlobData,
|
|
1319
|
-
remoteCallbacks,
|
|
1320
|
-
spawnRecordingContainer,
|
|
1321
1133
|
startRequest
|
|
1322
1134
|
};
|