@replayio-app-building/netlify-recorder 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -237,7 +237,6 @@ const handler: Handler = async (event) => {
237
237
 
238
238
  const recordingId = await ensureRequestRecording(requestId, {
239
239
  repositoryUrl: process.env.APP_REPOSITORY_URL!,
240
- replayApiKey: process.env.RECORD_REPLAY_API_KEY!,
241
240
  lookupRequest: async (id) => {
242
241
  const [row] = await sql`
243
242
  SELECT blob_url, commit_sha, branch_name, handler_path
@@ -312,6 +311,10 @@ Self-hosted recording requires these environment variables:
312
311
 
313
312
  Wraps a Netlify handler function with automatic request recording. This is the recommended way to integrate — it handles `startRequest`/`finishRequest` and error cleanup internally.
314
313
 
314
+ **Response timing:** When the Netlify Functions v2 `context` object is available (with `waitUntil`), the response is returned to the client **immediately** with a pre-generated `X-Replay-Request-Id` header. The blob upload and metadata storage continue in the background via `context.waitUntil()`, adding zero latency to the client response.
315
+
316
+ When `context.waitUntil` is not available (v1 handlers or missing context), the wrapper falls back to awaiting `finishRequest` before returning.
317
+
315
318
  **Parameters:**
316
319
  - `handler` — Your async handler function `(event, context?) => Promise<{ statusCode, headers?, body? }>`
317
320
  - `options.callbacks` — Either `remoteCallbacks(serviceUrl)` or custom `{ uploadBlob, storeRequestData }` callbacks
@@ -322,6 +325,8 @@ Wraps a Netlify handler function with automatic request recording. This is the r
322
325
 
323
326
  **Returns:** A wrapped handler function with the same signature.
324
327
 
328
+ **Callbacks note:** When using the `waitUntil` flow, `storeRequestData` receives a `requestId` field in its data parameter. Callbacks should use this as the row ID so the stored record matches the ID already sent to the client.
329
+
325
330
  ### `startRequest(event): RequestContext`
326
331
 
327
332
  Lower-level API. Begins capturing a Netlify handler execution. Patches `globalThis.fetch` and `process.env` to record all outbound network calls and environment variable reads. Use this with `finishRequest` when you need more control than `createRecordingRequestHandler` provides.
@@ -353,6 +358,7 @@ Logs a `console.warn` when the total duration exceeds 2 seconds or when individu
353
358
  - `options.commitSha` — Override `COMMIT_SHA` env var
354
359
  - `options.branchName` — Override `BRANCH_NAME` env var
355
360
  - `options.repositoryUrl` — Override `REPLAY_REPOSITORY_URL` env var
361
+ - `options.requestId` — Pre-generated request ID (used by `createRecordingRequestHandler` in the `waitUntil` flow)
356
362
 
357
363
  ### `remoteCallbacks(serviceUrl): FinishRequestCallbacks`
358
364
 
@@ -368,7 +374,6 @@ Spawns a container via `@replayio/app-building` to create a Replay recording fro
368
374
  **Parameters:**
369
375
  - `requestId` — The request to create a recording for
370
376
  - `options.repositoryUrl` — Git repository URL for the container to clone
371
- - `options.replayApiKey` — Replay API key for recording upload
372
377
  - `options.lookupRequest(id)` — Fetches `{ blobUrl, commitSha, branchName, handlerPath }` from the database
373
378
  - `options.updateStatus(id, status, recordingId?)` — Updates the request status in the database
374
379
 
package/dist/index.d.ts CHANGED
@@ -95,13 +95,22 @@ interface BlobData {
95
95
  interface FinishRequestCallbacks {
96
96
  /** Uploads serialized captured data and returns the blob URL. */
97
97
  uploadBlob: (data: string) => Promise<string>;
98
- /** Stores request metadata in the database and returns the request ID. */
98
+ /**
99
+ * Stores request metadata in the database and returns the request ID.
100
+ *
101
+ * When `requestId` is provided (from `createRecordingRequestHandler`'s
102
+ * `waitUntil` flow), the callback should use it as the row ID so the
103
+ * client-facing header and the stored record match. When omitted, the
104
+ * callback generates its own ID (backward-compatible).
105
+ */
99
106
  storeRequestData: (data: {
100
107
  blobUrl: string;
101
108
  commitSha: string;
102
109
  branchName: string;
103
110
  repositoryUrl: string;
104
111
  handlerPath: string;
112
+ /** Pre-generated request ID. Use as the row ID when provided. */
113
+ requestId?: string;
105
114
  }) => Promise<string>;
106
115
  }
107
116
  /**
@@ -126,7 +135,6 @@ interface ContainerInfraConfig {
126
135
  }
127
136
  interface EnsureRecordingOptions {
128
137
  repositoryUrl: string;
129
- replayApiKey: string;
130
138
  /** Infrastructure credentials for starting the recording container. */
131
139
  infraConfig?: ContainerInfraConfig;
132
140
  /** Webhook URL the container can POST log entries to (optional). */
@@ -200,6 +208,15 @@ interface FinishRequestOptions {
200
208
  branchName?: string;
201
209
  /** Override REPLAY_REPOSITORY_URL env var. */
202
210
  repositoryUrl?: string;
211
+ /**
212
+ * Pre-generated request ID. When provided, this ID is passed to the
213
+ * `storeRequestData` callback so the stored row matches the ID already
214
+ * sent to the client in the `X-Replay-Request-Id` header.
215
+ *
216
+ * Used by `createRecordingRequestHandler` in the `waitUntil` flow where
217
+ * the response is returned before `finishRequest` runs.
218
+ */
219
+ requestId?: string;
203
220
  }
204
221
  /**
205
222
  * Called at the end of the handler execution.
@@ -232,9 +249,14 @@ interface CreateRecordingRequestHandlerOptions extends FinishRequestOptions {
232
249
  * after, capturing all outbound network calls and environment variable reads.
233
250
  * On error, interceptors are cleaned up and the error is re-thrown.
234
251
  *
235
- * **Important:** You must use the response returned by the wrapped handler
236
- * (not your original response object), because it includes the
237
- * `X-Replay-Request-Id` header set by `finishRequest`.
252
+ * **Response timing:** When the Netlify Functions v2 `context` object is
253
+ * available (with `waitUntil`), the response is returned to the client
254
+ * **immediately** with a pre-generated `X-Replay-Request-Id` header. The
255
+ * blob upload and metadata storage continue in the background via
256
+ * `context.waitUntil()`. This avoids adding latency to the client response.
257
+ *
258
+ * When `context.waitUntil` is not available (v1 handlers or missing context),
259
+ * the wrapper falls back to awaiting `finishRequest` before returning.
238
260
  *
239
261
  * For v2 handlers the request body is read from a clone internally — your
240
262
  * handler still receives the original request with an unconsumed body.
@@ -380,8 +402,6 @@ interface SpawnRecordingContainerOptions {
380
402
  branchName: string;
381
403
  /** Git repository URL for the app. */
382
404
  repositoryUrl: string;
383
- /** Replay API key for uploading recordings. */
384
- replayApiKey: string;
385
405
  /** Infrastructure credentials for Fly.io + Infisical. */
386
406
  infraConfig: ContainerInfraConfig;
387
407
  /** Optional webhook URL the container can POST log events to. */
@@ -401,4 +421,46 @@ interface SpawnRecordingContainerOptions {
401
421
  */
402
422
  declare function spawnRecordingContainer(options: SpawnRecordingContainerOptions): Promise<string>;
403
423
 
404
- export { type BlobData, type CapturedData, type ContainerInfraConfig, type CreateRecordingRequestHandlerOptions, type EnsureRecordingOptions, type EnvRead, type FinishRequestCallbacks, type FinishRequestOptions, type HandlerResponse$1 as HandlerResponse, type NetlifyEvent, type NetlifyV2Request, type NetworkCall, type RecordingResult, type RequestContext, type RequestInfo, type SpawnRecordingContainerOptions, createRecordingRequestHandler, createRequestRecording, ensureRequestRecording, finishRequest, readInfraConfigFromEnv, redactBlobData, remoteCallbacks, spawnRecordingContainer, startRequest };
424
+ type SqlFunction = (...args: any[]) => Promise<any[]>;
425
+ /**
426
+ * Creates the `audit_log` table and a generic PL/pgSQL trigger function
427
+ * (`audit_trigger_function`) that records INSERT, UPDATE, and DELETE
428
+ * operations on any monitored table.
429
+ *
430
+ * Call this once during schema initialization.
431
+ */
432
+ declare function databaseAuditEnsureLogTable(sql: SqlFunction): Promise<void>;
433
+ /**
434
+ * Creates a trigger on the specified table that calls
435
+ * `audit_trigger_function` for INSERT, UPDATE, and DELETE operations.
436
+ *
437
+ * Throws if `tableName` is `'audit_log'` (cannot monitor itself).
438
+ */
439
+ declare function databaseAuditMonitorTable(sql: SqlFunction, tableName: string): Promise<void>;
440
+ /**
441
+ * Returns all rows from the `audit_log` table, ordered by `performed_at` DESC.
442
+ */
443
+ declare function databaseAuditDumpLogTable(sql: SqlFunction): Promise<Record<string, unknown>[]>;
444
+ /**
445
+ * Wraps a Neon SQL tagged-template function so that after each query,
446
+ * any new `audit_log` rows (with NULL `replay_request_id`) are tagged
447
+ * with the current request ID and an incrementing call index.
448
+ *
449
+ * The request ID is read from module-level state set by
450
+ * `createRecordingRequestHandler`. If no request is active, the
451
+ * wrapper passes calls through without tagging.
452
+ *
453
+ * @example
454
+ * ```ts
455
+ * import { createAuditedSql } from "@replayio-app-building/netlify-recorder";
456
+ *
457
+ * const sql = createAuditedSql(getSql());
458
+ * await sql`INSERT INTO haikus (text) VALUES (${haiku})`;
459
+ * // audit_log entries from this INSERT are now tagged with the request ID
460
+ * ```
461
+ */
462
+ declare function createAuditedSql(sql: SqlFunction): SqlFunction;
463
+
464
+ declare function getCurrentRequestId(): string | null;
465
+
466
+ export { type BlobData, type CapturedData, type ContainerInfraConfig, type CreateRecordingRequestHandlerOptions, type EnsureRecordingOptions, type EnvRead, type FinishRequestCallbacks, type FinishRequestOptions, type HandlerResponse$1 as HandlerResponse, type NetlifyEvent, type NetlifyV2Request, type NetworkCall, type RecordingResult, type RequestContext, type RequestInfo, type SpawnRecordingContainerOptions, createAuditedSql, createRecordingRequestHandler, createRequestRecording, databaseAuditDumpLogTable, databaseAuditEnsureLogTable, databaseAuditMonitorTable, ensureRequestRecording, finishRequest, getCurrentRequestId, readInfraConfigFromEnv, redactBlobData, remoteCallbacks, spawnRecordingContainer, startRequest };
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
8
8
  // src/interceptors/network.ts
9
9
  function installNetworkInterceptor(mode, calls) {
10
10
  const originalFetch = globalThis.fetch;
11
- let replayCallIndex = 0;
11
+ const consumed = /* @__PURE__ */ new Set();
12
12
  if (mode === "capture") {
13
13
  const captureFetch = async (input, init) => {
14
14
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
@@ -40,16 +40,35 @@ function installNetworkInterceptor(mode, calls) {
40
40
  };
41
41
  globalThis.fetch = captureFetch;
42
42
  } else {
43
- const replayFetch = async () => {
44
- const idx = replayCallIndex++;
45
- const call = calls[idx];
46
- if (!call) {
43
+ const replayFetch = async (input, init) => {
44
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : typeof input === "object" && input !== null && "url" in input ? input.url : String(input);
45
+ const requestBody = typeof init?.body === "string" ? init.body : void 0;
46
+ let matchIdx = -1;
47
+ for (let i = 0; i < calls.length; i++) {
48
+ if (consumed.has(i)) continue;
49
+ const c = calls[i];
50
+ if (c && c.url === url && c.requestBody === requestBody) {
51
+ matchIdx = i;
52
+ break;
53
+ }
54
+ }
55
+ if (matchIdx === -1) {
56
+ for (let i = 0; i < calls.length; i++) {
57
+ if (!consumed.has(i)) {
58
+ matchIdx = i;
59
+ break;
60
+ }
61
+ }
62
+ }
63
+ const call = calls[matchIdx];
64
+ if (matchIdx === -1 || !call) {
47
65
  throw new Error(
48
66
  `No more recorded network calls to replay (exhausted ${calls.length} calls)`
49
67
  );
50
68
  }
69
+ consumed.add(matchIdx);
51
70
  console.log(
52
- ` [network-replay] Consumed call ${idx + 1}/${calls.length}: ${call.method} ${call.url} => ${call.responseStatus}`
71
+ ` [network-replay] Consumed call ${consumed.size}/${calls.length}: ${call.method} ${call.url} => ${call.responseStatus}`
53
72
  );
54
73
  const body = call.responseBody ?? "";
55
74
  const status = call.responseStatus;
@@ -88,10 +107,17 @@ function installNetworkInterceptor(mode, calls) {
88
107
  globalThis.fetch = originalFetch;
89
108
  },
90
109
  consumedCount() {
91
- return replayCallIndex;
110
+ return consumed.size;
92
111
  },
93
112
  totalCount() {
94
113
  return calls.length;
114
+ },
115
+ unconsumedIndices() {
116
+ const indices = [];
117
+ for (let i = 0; i < calls.length; i++) {
118
+ if (!consumed.has(i)) indices.push(i);
119
+ }
120
+ return indices;
95
121
  }
96
122
  };
97
123
  }
@@ -427,7 +453,8 @@ async function finishRequest(requestContext, callbacks, response, options) {
427
453
  commitSha,
428
454
  branchName,
429
455
  repositoryUrl,
430
- handlerPath
456
+ handlerPath,
457
+ requestId: options?.requestId
431
458
  });
432
459
  const storeDuration = Date.now() - storeStart;
433
460
  if (storeDuration > SLOW_STEP_THRESHOLD_MS) {
@@ -450,17 +477,58 @@ async function finishRequest(requestContext, callbacks, response, options) {
450
477
  };
451
478
  }
452
479
 
480
+ // src/createRecordingRequestHandler.ts
481
+ import crypto from "crypto";
482
+
483
+ // src/requestState.ts
484
+ var _currentRequestId = null;
485
+ function setCurrentRequestId(id) {
486
+ _currentRequestId = id;
487
+ }
488
+ function getCurrentRequestId() {
489
+ return _currentRequestId;
490
+ }
491
+
453
492
  // src/createRecordingRequestHandler.ts
454
493
  function createRecordingRequestHandler(handler, options) {
455
494
  return async (event, context) => {
495
+ const requestId = crypto.randomUUID();
496
+ setCurrentRequestId(requestId);
456
497
  const reqContext = startRequest(event);
498
+ let response;
457
499
  try {
458
- const response = await handler(event, context);
459
- return await finishRequest(reqContext, options.callbacks, response, options);
500
+ response = await handler(event, context);
460
501
  } catch (err) {
502
+ setCurrentRequestId(null);
461
503
  reqContext.cleanup();
462
504
  throw err;
463
505
  }
506
+ reqContext.cleanup();
507
+ setCurrentRequestId(null);
508
+ const responseWithHeader = {
509
+ ...response,
510
+ headers: {
511
+ ...response.headers,
512
+ "X-Replay-Request-Id": requestId
513
+ }
514
+ };
515
+ const finishOpts = { ...options, requestId };
516
+ const ctx = context;
517
+ if (ctx && typeof ctx.waitUntil === "function") {
518
+ ctx.waitUntil(
519
+ finishRequest(reqContext, options.callbacks, response, finishOpts).catch(
520
+ (err) => {
521
+ console.error(
522
+ `netlify-recorder: background finishRequest failed (handler: ${options.handlerPath ?? "unknown"})`,
523
+ err
524
+ );
525
+ }
526
+ )
527
+ );
528
+ return responseWithHeader;
529
+ }
530
+ await finishRequest(reqContext, options.callbacks, response, finishOpts);
531
+ return responseWithHeader;
464
532
  };
465
533
  }
466
534
 
@@ -491,7 +559,8 @@ function remoteCallbacks(serviceUrl) {
491
559
  handlerPath: metadata.handlerPath,
492
560
  commitSha: metadata.commitSha,
493
561
  branchName: metadata.branchName,
494
- repositoryUrl: metadata.repositoryUrl
562
+ repositoryUrl: metadata.repositoryUrl,
563
+ requestId: metadata.requestId
495
564
  })
496
565
  }
497
566
  );
@@ -515,7 +584,6 @@ async function spawnRecordingContainer(options) {
515
584
  commitSha,
516
585
  branchName,
517
586
  repositoryUrl,
518
- replayApiKey,
519
587
  infraConfig,
520
588
  logWebhookUrl,
521
589
  onLog
@@ -605,7 +673,7 @@ async function spawnRecordingContainer(options) {
605
673
  `DB operations that were not in the original blob. Do NOT try to fix these.`,
606
674
  ``,
607
675
  `=== Step 6: Upload the recording ===`,
608
- `RECORD_REPLAY_API_KEY=${replayApiKey} npx replayio upload --all 2>&1`,
676
+ `exec-secrets RECORD_REPLAY_API_KEY -- npx replayio upload --all 2>&1`,
609
677
  ``,
610
678
  `Find the recording ID (UUID) in the upload output and print:`,
611
679
  ` recording: <recording-id>`,
@@ -671,7 +739,7 @@ async function spawnRecordingContainer(options) {
671
739
 
672
740
  // src/ensureRequestRecording.ts
673
741
  async function ensureRequestRecording(requestId, options) {
674
- const { repositoryUrl, replayApiKey, infraConfig, webhookUrl, lookupRequest, updateStatus, onLog } = options;
742
+ const { repositoryUrl, infraConfig, webhookUrl, lookupRequest, updateStatus, onLog } = options;
675
743
  const emit = async (level, message) => {
676
744
  if (onLog) {
677
745
  try {
@@ -696,7 +764,6 @@ async function ensureRequestRecording(requestId, options) {
696
764
  commitSha: requestData.commitSha,
697
765
  branchName: requestData.branchName,
698
766
  repositoryUrl,
699
- replayApiKey,
700
767
  infraConfig,
701
768
  logWebhookUrl: webhookUrl,
702
769
  onLog
@@ -879,6 +946,12 @@ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo) {
879
946
  this.headers = new H(init?.headers);
880
947
  this.ok = this.status >= 200 && this.status < 300;
881
948
  }
949
+ static json(data, init) {
950
+ const body = JSON.stringify(data);
951
+ const headers = init?.headers ?? {};
952
+ headers["content-type"] = "application/json";
953
+ return new ResponseShim(body, { ...init, headers });
954
+ }
882
955
  async text() {
883
956
  return this._body ?? "";
884
957
  }
@@ -1002,17 +1075,18 @@ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo) {
1002
1075
  }
1003
1076
  }
1004
1077
  } finally {
1005
- const consumed = networkHandle.consumedCount();
1078
+ const consumedCount = networkHandle.consumedCount();
1006
1079
  const total = networkHandle.totalCount();
1007
- if (consumed < total) {
1080
+ if (consumedCount < total) {
1008
1081
  result.unconsumedNetworkCalls = true;
1082
+ const unconsumed = networkHandle.unconsumedIndices();
1009
1083
  const details = [
1010
- `Consumed ${consumed} of ${total} calls. ${total - consumed} call(s) were never replayed.`
1084
+ `Consumed ${consumedCount} of ${total} calls. ${unconsumed.length} call(s) were never replayed.`
1011
1085
  ];
1012
1086
  console.error(
1013
- `ERROR: Not all recorded network calls were consumed during replay. Consumed ${consumed} of ${total} calls. ${total - consumed} call(s) were never replayed.`
1087
+ `ERROR: Not all recorded network calls were consumed during replay. Consumed ${consumedCount} of ${total} calls. ${unconsumed.length} call(s) were never replayed.`
1014
1088
  );
1015
- for (let i = consumed; i < total; i++) {
1089
+ for (const i of unconsumed) {
1016
1090
  const call = blobData.capturedData.networkCalls[i];
1017
1091
  if (call) {
1018
1092
  details.push(`Unconsumed: ${call.method} ${call.url} => ${call.responseStatus}`);
@@ -1031,11 +1105,107 @@ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo) {
1031
1105
  }
1032
1106
  return result;
1033
1107
  }
1108
+
1109
+ // src/databaseAudit.ts
1110
+ async function databaseAuditEnsureLogTable(sql) {
1111
+ await sql`
1112
+ CREATE TABLE IF NOT EXISTS audit_log (
1113
+ id BIGSERIAL PRIMARY KEY,
1114
+ table_name TEXT NOT NULL,
1115
+ record_id TEXT NOT NULL,
1116
+ action TEXT NOT NULL,
1117
+ old_data JSONB,
1118
+ new_data JSONB,
1119
+ changed_fields TEXT[],
1120
+ performed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1121
+ replay_request_id TEXT,
1122
+ replay_request_call_index INTEGER
1123
+ )
1124
+ `;
1125
+ await sql`CREATE INDEX IF NOT EXISTS idx_audit_log_table_name ON audit_log (table_name)`;
1126
+ await sql`CREATE INDEX IF NOT EXISTS idx_audit_log_replay_request_id ON audit_log (replay_request_id)`;
1127
+ await sql`CREATE INDEX IF NOT EXISTS idx_audit_log_performed_at ON audit_log (performed_at DESC)`;
1128
+ await sql`
1129
+ CREATE OR REPLACE FUNCTION audit_trigger_function()
1130
+ RETURNS TRIGGER AS $$
1131
+ DECLARE
1132
+ changed_cols TEXT[];
1133
+ BEGIN
1134
+ IF TG_OP = 'INSERT' THEN
1135
+ INSERT INTO audit_log (table_name, record_id, action, new_data)
1136
+ VALUES (TG_TABLE_NAME, NEW.id::TEXT, 'INSERT', to_jsonb(NEW));
1137
+ RETURN NEW;
1138
+ ELSIF TG_OP = 'UPDATE' THEN
1139
+ SELECT ARRAY_AGG(n.key) INTO changed_cols
1140
+ FROM jsonb_each_text(to_jsonb(NEW)) n
1141
+ LEFT JOIN jsonb_each_text(to_jsonb(OLD)) o ON n.key = o.key
1142
+ WHERE o.value IS DISTINCT FROM n.value;
1143
+
1144
+ INSERT INTO audit_log (table_name, record_id, action, old_data, new_data, changed_fields)
1145
+ VALUES (TG_TABLE_NAME, NEW.id::TEXT, 'UPDATE', to_jsonb(OLD), to_jsonb(NEW), COALESCE(changed_cols, ARRAY[]::TEXT[]));
1146
+ RETURN NEW;
1147
+ ELSIF TG_OP = 'DELETE' THEN
1148
+ INSERT INTO audit_log (table_name, record_id, action, old_data)
1149
+ VALUES (TG_TABLE_NAME, OLD.id::TEXT, 'DELETE', to_jsonb(OLD));
1150
+ RETURN OLD;
1151
+ END IF;
1152
+ RETURN NULL;
1153
+ END;
1154
+ $$ LANGUAGE plpgsql
1155
+ `;
1156
+ }
1157
+ async function databaseAuditMonitorTable(sql, tableName) {
1158
+ if (tableName === "audit_log") {
1159
+ throw new Error("Cannot monitor the audit_log table itself");
1160
+ }
1161
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
1162
+ throw new Error(`Invalid table name: ${tableName}`);
1163
+ }
1164
+ const triggerName = `audit_trigger_${tableName}`;
1165
+ await sql(
1166
+ `DROP TRIGGER IF EXISTS ${triggerName} ON "${tableName}"`,
1167
+ []
1168
+ );
1169
+ await sql(
1170
+ `CREATE TRIGGER ${triggerName} AFTER INSERT OR UPDATE OR DELETE ON "${tableName}" FOR EACH ROW EXECUTE FUNCTION audit_trigger_function()`,
1171
+ []
1172
+ );
1173
+ }
1174
+ async function databaseAuditDumpLogTable(sql) {
1175
+ const rows = await sql`SELECT * FROM audit_log ORDER BY performed_at DESC`;
1176
+ return rows;
1177
+ }
1178
+ function createAuditedSql(sql) {
1179
+ let callIndex = 0;
1180
+ const requestId = getCurrentRequestId();
1181
+ const auditedSql = async (strings, ...values) => {
1182
+ const result = await sql(strings, ...values);
1183
+ callIndex++;
1184
+ if (requestId) {
1185
+ try {
1186
+ await sql`
1187
+ UPDATE audit_log
1188
+ SET replay_request_id = ${requestId}, replay_request_call_index = ${callIndex}
1189
+ WHERE replay_request_id IS NULL
1190
+ `;
1191
+ } catch (err) {
1192
+ console.warn("netlify-recorder: failed to update audit log:", err);
1193
+ }
1194
+ }
1195
+ return result;
1196
+ };
1197
+ return auditedSql;
1198
+ }
1034
1199
  export {
1200
+ createAuditedSql,
1035
1201
  createRecordingRequestHandler,
1036
1202
  createRequestRecording,
1203
+ databaseAuditDumpLogTable,
1204
+ databaseAuditEnsureLogTable,
1205
+ databaseAuditMonitorTable,
1037
1206
  ensureRequestRecording,
1038
1207
  finishRequest,
1208
+ getCurrentRequestId,
1039
1209
  readInfraConfigFromEnv,
1040
1210
  redactBlobData,
1041
1211
  remoteCallbacks,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replayio-app-building/netlify-recorder",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Capture and replay Netlify function executions as Replay recordings",
5
5
  "type": "module",
6
6
  "exports": {