@replayio-app-building/netlify-recorder 0.12.0 → 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.
package/dist/index.d.ts CHANGED
@@ -442,13 +442,14 @@ declare function databaseAuditMonitorTable(sql: SqlFunction, tableName: string):
442
442
  */
443
443
  declare function databaseAuditDumpLogTable(sql: SqlFunction): Promise<Record<string, unknown>[]>;
444
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.
445
+ * Wraps a Neon SQL tagged-template function so that each query runs
446
+ * inside a transaction that first calls `set_config()` to inject the
447
+ * current request ID and call index. The `audit_trigger_function`
448
+ * reads these via `current_setting()` and stamps audit rows atomically
449
+ * — no separate UPDATE required.
448
450
  *
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.
451
+ * Requires the Neon HTTP driver's `.transaction()` method so that
452
+ * `set_config` and the user's query share one transaction.
452
453
  *
453
454
  * @example
454
455
  * ```ts
@@ -456,7 +457,7 @@ declare function databaseAuditDumpLogTable(sql: SqlFunction): Promise<Record<str
456
457
  *
457
458
  * const sql = createAuditedSql(getSql());
458
459
  * await sql`INSERT INTO haikus (text) VALUES (${haiku})`;
459
- * // audit_log entries from this INSERT are now tagged with the request ID
460
+ * // audit_log row created by the trigger already has the request ID
460
461
  * ```
461
462
  */
462
463
  declare function createAuditedSql(sql: SqlFunction): SqlFunction;
package/dist/index.js CHANGED
@@ -1130,10 +1130,22 @@ async function databaseAuditEnsureLogTable(sql) {
1130
1130
  RETURNS TRIGGER AS $$
1131
1131
  DECLARE
1132
1132
  changed_cols TEXT[];
1133
+ req_id TEXT;
1134
+ call_idx INTEGER;
1133
1135
  BEGIN
1136
+ -- Read application context set via SET LOCAL by createAuditedSql
1137
+ req_id := COALESCE(current_setting('app.replay_request_id', true), '');
1138
+ IF req_id = '' THEN req_id := NULL; END IF;
1139
+
1140
+ BEGIN
1141
+ call_idx := current_setting('app.replay_call_index', true)::INTEGER;
1142
+ EXCEPTION WHEN OTHERS THEN
1143
+ call_idx := NULL;
1144
+ END;
1145
+
1134
1146
  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));
1147
+ INSERT INTO audit_log (table_name, record_id, action, new_data, replay_request_id, replay_request_call_index)
1148
+ VALUES (TG_TABLE_NAME, NEW.id::TEXT, 'INSERT', to_jsonb(NEW), req_id, call_idx);
1137
1149
  RETURN NEW;
1138
1150
  ELSIF TG_OP = 'UPDATE' THEN
1139
1151
  SELECT ARRAY_AGG(n.key) INTO changed_cols
@@ -1141,12 +1153,12 @@ async function databaseAuditEnsureLogTable(sql) {
1141
1153
  LEFT JOIN jsonb_each_text(to_jsonb(OLD)) o ON n.key = o.key
1142
1154
  WHERE o.value IS DISTINCT FROM n.value;
1143
1155
 
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[]));
1156
+ INSERT INTO audit_log (table_name, record_id, action, old_data, new_data, changed_fields, replay_request_id, replay_request_call_index)
1157
+ VALUES (TG_TABLE_NAME, NEW.id::TEXT, 'UPDATE', to_jsonb(OLD), to_jsonb(NEW), COALESCE(changed_cols, ARRAY[]::TEXT[]), req_id, call_idx);
1146
1158
  RETURN NEW;
1147
1159
  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));
1160
+ INSERT INTO audit_log (table_name, record_id, action, old_data, replay_request_id, replay_request_call_index)
1161
+ VALUES (TG_TABLE_NAME, OLD.id::TEXT, 'DELETE', to_jsonb(OLD), req_id, call_idx);
1150
1162
  RETURN OLD;
1151
1163
  END IF;
1152
1164
  RETURN NULL;
@@ -1177,22 +1189,23 @@ async function databaseAuditDumpLogTable(sql) {
1177
1189
  }
1178
1190
  function createAuditedSql(sql) {
1179
1191
  let callIndex = 0;
1180
- const requestId = getCurrentRequestId();
1192
+ const sqlWithTx = sql;
1181
1193
  const auditedSql = async (strings, ...values) => {
1182
- const result = await sql(strings, ...values);
1194
+ const requestId = getCurrentRequestId();
1183
1195
  callIndex++;
1184
- if (requestId) {
1196
+ if (requestId && typeof sqlWithTx.transaction === "function") {
1185
1197
  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
- `;
1198
+ const results = await sqlWithTx.transaction([
1199
+ sql`SELECT set_config('app.replay_request_id', ${requestId}, true)`,
1200
+ sql`SELECT set_config('app.replay_call_index', ${String(callIndex)}, true)`,
1201
+ sql(strings, ...values)
1202
+ ]);
1203
+ return results[2];
1191
1204
  } catch (err) {
1192
- console.warn("netlify-recorder: failed to update audit log:", err);
1205
+ console.warn("netlify-recorder: audited transaction failed, falling back:", err);
1193
1206
  }
1194
1207
  }
1195
- return result;
1208
+ return await sql(strings, ...values);
1196
1209
  };
1197
1210
  return auditedSql;
1198
1211
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replayio-app-building/netlify-recorder",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Capture and replay Netlify function executions as Replay recordings",
5
5
  "type": "module",
6
6
  "exports": {