@replayio-app-building/netlify-recorder 0.15.1 → 0.15.3

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 (3) hide show
  1. package/README.md +117 -0
  2. package/dist/index.js +16 -3
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -305,6 +305,90 @@ Self-hosted recording requires these environment variables:
305
305
 
306
306
  ---
307
307
 
308
+ ## Audit Log Support
309
+
310
+ The package includes helpers that automatically track database mutations (INSERT, UPDATE, DELETE) in an `audit_log` table and link each change to the Replay request that caused it. This makes it easy to trace exactly which incoming request triggered a given database change.
311
+
312
+ ### Setup
313
+
314
+ #### 1. Create the audit log table
315
+
316
+ Call `databaseAuditEnsureLogTable` once during schema initialization. This creates the `audit_log` table, its indexes, and a reusable PL/pgSQL trigger function:
317
+
318
+ ```typescript
319
+ import { databaseAuditEnsureLogTable } from "@replayio-app-building/netlify-recorder";
320
+
321
+ await databaseAuditEnsureLogTable(sql);
322
+ ```
323
+
324
+ #### 2. Monitor tables
325
+
326
+ For each table you want to audit, call `databaseAuditMonitorTable`. This attaches a trigger that logs every INSERT, UPDATE, and DELETE:
327
+
328
+ ```typescript
329
+ import { databaseAuditMonitorTable } from "@replayio-app-building/netlify-recorder";
330
+
331
+ await databaseAuditMonitorTable(sql, "users");
332
+ await databaseAuditMonitorTable(sql, "orders");
333
+ ```
334
+
335
+ #### 3. Use audited SQL in your handlers
336
+
337
+ Wrap your Neon SQL function with `createAuditedSql` inside handlers that are wrapped with `createRecordingRequestHandler`. This ensures each audit log entry is stamped with the current Replay request ID and a per-request call index:
338
+
339
+ ```typescript
340
+ import {
341
+ createRecordingRequestHandler,
342
+ remoteCallbacks,
343
+ createAuditedSql,
344
+ } from "@replayio-app-building/netlify-recorder";
345
+
346
+ export default createRecordingRequestHandler(
347
+ async (req) => {
348
+ const auditedSql = createAuditedSql(sql);
349
+ // Queries through auditedSql automatically tag audit_log rows
350
+ // with the replay request ID — no extra code needed.
351
+ await auditedSql`INSERT INTO orders (product, qty) VALUES (${product}, ${qty})`;
352
+
353
+ return { statusCode: 200, body: "OK" };
354
+ },
355
+ {
356
+ callbacks: remoteCallbacks(RECORDER_URL),
357
+ handlerPath: "netlify/functions/create-order",
358
+ }
359
+ );
360
+ ```
361
+
362
+ #### 4. Query the audit log
363
+
364
+ Use `databaseAuditDumpLogTable` to retrieve all audit entries (ordered by most recent first):
365
+
366
+ ```typescript
367
+ import { databaseAuditDumpLogTable } from "@replayio-app-building/netlify-recorder";
368
+
369
+ const entries = await databaseAuditDumpLogTable(sql);
370
+ ```
371
+
372
+ Each entry contains:
373
+
374
+ | Field | Description |
375
+ |---|---|
376
+ | `table_name` | The table where the change occurred |
377
+ | `record_id` | The `id` of the affected row |
378
+ | `action` | `INSERT`, `UPDATE`, or `DELETE` |
379
+ | `old_data` | Previous row data (UPDATE/DELETE only) |
380
+ | `new_data` | New row data (INSERT/UPDATE only) |
381
+ | `changed_fields` | Array of column names that changed (UPDATE only) |
382
+ | `performed_at` | Timestamp of the change |
383
+ | `replay_request_id` | The Replay request ID that caused the change (when using `createAuditedSql`) |
384
+ | `replay_request_call_index` | The sequential query index within the request |
385
+
386
+ ### How it works
387
+
388
+ `createAuditedSql` wraps each query in a transaction that first calls `SET LOCAL` to inject the current request ID and call index into PostgreSQL session variables. The trigger function reads these via `current_setting()` and stamps audit rows atomically — no separate UPDATE required. If no request context is available (e.g. queries outside a handler), the query executes normally without audit metadata.
389
+
390
+ ---
391
+
308
392
  ## API Reference
309
393
 
310
394
  ### `createRecordingRequestHandler(handler, options): Handler`
@@ -386,6 +470,39 @@ Called inside a container running under `replay-node`. Downloads the captured da
386
470
  - `handlerPath` — Path to the handler module to execute
387
471
  - `requestInfo` — The original request info to replay
388
472
 
473
+ ### `databaseAuditEnsureLogTable(sql): Promise<void>`
474
+
475
+ Creates the `audit_log` table, its indexes, and a reusable PL/pgSQL trigger function (`audit_trigger_function`). Call once during schema initialization.
476
+
477
+ **Parameters:**
478
+ - `sql` — A Neon SQL tagged-template function
479
+
480
+ ### `databaseAuditMonitorTable(sql, tableName): Promise<void>`
481
+
482
+ Attaches a trigger to the specified table that logs INSERT, UPDATE, and DELETE operations to the `audit_log` table. Throws if `tableName` is `'audit_log'`.
483
+
484
+ **Parameters:**
485
+ - `sql` — A Neon SQL tagged-template function
486
+ - `tableName` — Name of the table to monitor (must match `^[a-zA-Z_][a-zA-Z0-9_]*$`)
487
+
488
+ ### `databaseAuditDumpLogTable(sql): Promise<Record<string, unknown>[]>`
489
+
490
+ Returns all rows from the `audit_log` table, ordered by `performed_at` DESC.
491
+
492
+ **Parameters:**
493
+ - `sql` — A Neon SQL tagged-template function
494
+
495
+ ### `createAuditedSql(sql): SqlFunction`
496
+
497
+ Wraps a Neon SQL tagged-template function so that each query runs inside a transaction that injects the current Replay request ID and call index via `SET LOCAL`. The audit trigger reads these values and stamps audit rows automatically.
498
+
499
+ Falls back to non-audited execution when no request context is active or if the transaction fails.
500
+
501
+ **Parameters:**
502
+ - `sql` — A Neon SQL tagged-template function (must have a `.transaction()` method, as provided by the Neon HTTP driver)
503
+
504
+ **Returns:** A wrapped SQL function with the same tagged-template interface.
505
+
389
506
  ## Environment Variables
390
507
 
391
508
  ### Required for all setups (read by `finishRequest`)
package/dist/index.js CHANGED
@@ -126,6 +126,10 @@ function installNetworkInterceptor(mode, calls) {
126
126
  function installEnvironmentInterceptor(mode, reads) {
127
127
  const originalEnv = process.env;
128
128
  if (mode === "capture") {
129
+ const now = Date.now();
130
+ for (const key of Object.keys(originalEnv)) {
131
+ reads.push({ key, value: originalEnv[key], timestamp: now });
132
+ }
129
133
  process.env = new Proxy(originalEnv, {
130
134
  get(target, prop) {
131
135
  const value = target[prop];
@@ -862,6 +866,16 @@ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo) {
862
866
  };
863
867
  g.File = FileShim;
864
868
  }
869
+ if (typeof g.crypto === "undefined") {
870
+ try {
871
+ const nodeCrypto = __require("crypto");
872
+ g.crypto = {
873
+ randomUUID: () => nodeCrypto.randomUUID(),
874
+ getRandomValues: (buf) => nodeCrypto.getRandomValues(buf)
875
+ };
876
+ } catch {
877
+ }
878
+ }
865
879
  if (typeof g.Headers === "undefined") {
866
880
  const HeadersShim = class Headers {
867
881
  _map;
@@ -1174,12 +1188,11 @@ async function databaseAuditMonitorTable(sql, tableName) {
1174
1188
  throw new Error(`Invalid table name: ${tableName}`);
1175
1189
  }
1176
1190
  const triggerName = `audit_trigger_${tableName}`;
1177
- const asTemplate = (s) => Object.assign([s], { raw: [s] });
1178
1191
  await sql(
1179
- asTemplate(`DROP TRIGGER IF EXISTS ${triggerName} ON "${tableName}"`)
1192
+ `DROP TRIGGER IF EXISTS ${triggerName} ON "${tableName}"`
1180
1193
  );
1181
1194
  await sql(
1182
- asTemplate(`CREATE TRIGGER ${triggerName} AFTER INSERT OR UPDATE OR DELETE ON "${tableName}" FOR EACH ROW EXECUTE FUNCTION audit_trigger_function()`)
1195
+ `CREATE TRIGGER ${triggerName} AFTER INSERT OR UPDATE OR DELETE ON "${tableName}" FOR EACH ROW EXECUTE FUNCTION audit_trigger_function()`
1183
1196
  );
1184
1197
  }
1185
1198
  async function databaseAuditDumpLogTable(sql) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replayio-app-building/netlify-recorder",
3
- "version": "0.15.1",
3
+ "version": "0.15.3",
4
4
  "description": "Capture and replay Netlify function executions as Replay recordings",
5
5
  "type": "module",
6
6
  "exports": {