@reproapp/node-sdk 0.0.48 → 0.0.49

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
@@ -299,6 +299,7 @@ type RequestCaptureScopeState = {
299
299
  privacyPolicyFingerprint?: string;
300
300
  privacyStatementCount: number;
301
301
  emittedValueDefinitionIds: Set<string>;
302
+ traceObservationSnapshots: Map<string, any>;
302
303
  };
303
304
  type BackendLiveTimingConfig = {
304
305
  sessionMaxDurationMs: number;
package/dist/index.js CHANGED
@@ -839,6 +839,7 @@ function createRequestCaptureScope(requestRid, transport, cfg) {
839
839
  privacyPolicyFingerprint: privacy?.fingerprint,
840
840
  privacyStatementCount: privacy?.statements.length ?? 0,
841
841
  emittedValueDefinitionIds: new Set(),
842
+ traceObservationSnapshots: new Map(),
842
843
  };
843
844
  }
844
845
  function recordRequestCaptureFailure(scope, stage, target, reason) {
@@ -2810,6 +2811,61 @@ function normalizeRawTraceArgs(values) {
2810
2811
  function hasMeaningfulRawTraceError(error) {
2811
2812
  return error !== undefined && error !== null;
2812
2813
  }
2814
+ const TRACE_OBSERVATION_SNAPSHOT = Symbol('repro.traceObservationSnapshot');
2815
+ // Trace events can remain queued after user code mutates the observed objects.
2816
+ // Intern immutable semantic snapshots per request so temporal evidence stays
2817
+ // correct without retaining another full copy for repeated unchanged values.
2818
+ function internTraceObservationSnapshot(snapshot, captureScope) {
2819
+ if (!captureScope || snapshot === null || typeof snapshot !== 'object') {
2820
+ return snapshot;
2821
+ }
2822
+ const serialized = stableSerializeTraceValue(snapshot);
2823
+ if (serialized === null)
2824
+ return snapshot;
2825
+ const digest = (0, crypto_1.createHash)('sha256')
2826
+ .update('repro-trace-observation-v1\0')
2827
+ .update(serialized)
2828
+ .digest('hex');
2829
+ const existing = captureScope.traceObservationSnapshots.get(digest);
2830
+ if (existing !== undefined)
2831
+ return existing;
2832
+ captureScope.traceObservationSnapshots.set(digest, snapshot);
2833
+ return snapshot;
2834
+ }
2835
+ function snapshotTraceValueAtObservation(value, captureScope, valuePath = '$', target = 'trace.value') {
2836
+ try {
2837
+ const snapshot = sanitizeTraceValue(value, 0, new WeakMap(), { preserveLongStrings: true, disableTruncation: true }, valuePath);
2838
+ return internTraceObservationSnapshot(snapshot, captureScope);
2839
+ }
2840
+ catch {
2841
+ recordRequestCaptureFailure(captureScope, 'trace.snapshot', target, 'snapshot_failed');
2842
+ return makeOmittedValue('capture-snapshot-failed');
2843
+ }
2844
+ }
2845
+ function snapshotTraceArgsAtObservation(values, captureScope) {
2846
+ const snapshot = snapshotTraceValueAtObservation(normalizeRawTraceArgs(values), captureScope, '$', 'trace.args');
2847
+ return Array.isArray(snapshot) ? snapshot : [snapshot];
2848
+ }
2849
+ function snapshotRawTraceEventAtObservation(raw, captureScope) {
2850
+ if (!raw || typeof raw !== 'object' || raw[TRACE_OBSERVATION_SNAPSHOT] === true) {
2851
+ return raw;
2852
+ }
2853
+ const snapshot = { ...raw };
2854
+ if (raw.args !== undefined) {
2855
+ snapshot.args = snapshotTraceArgsAtObservation(raw.args, captureScope);
2856
+ }
2857
+ if (raw.returnValue !== undefined) {
2858
+ snapshot.returnValue = snapshotTraceValueAtObservation(raw.returnValue, captureScope, '$', 'trace.returnValue');
2859
+ }
2860
+ if (hasMeaningfulRawTraceError(raw.error)) {
2861
+ snapshot.error = snapshotTraceValueAtObservation(raw.error, captureScope, '$', 'trace.error');
2862
+ }
2863
+ Object.defineProperty(snapshot, TRACE_OBSERVATION_SNAPSHOT, {
2864
+ value: true,
2865
+ enumerable: false,
2866
+ });
2867
+ return snapshot;
2868
+ }
2813
2869
  function attachTraceValueEntry(event, entry) {
2814
2870
  const current = event[TRACE_FULL_VALUE_ENTRY_SYMBOL];
2815
2871
  if (Array.isArray(current)) {
@@ -4774,21 +4830,25 @@ function reproMiddleware(cfg) {
4774
4830
  res.on('close', endTrackedClientRequest);
4775
4831
  return next(); // only capture tagged requests
4776
4832
  }
4777
- const captureCfg = backendLiveChild
4778
- ? {
4833
+ let captureCfg = cfg;
4834
+ if (backendLiveChild) {
4835
+ captureCfg = {
4779
4836
  ...cfg,
4780
4837
  actorId: backendLiveChild.actorId,
4781
4838
  actorType: backendLiveChild.actorType,
4782
4839
  actorLabels: backendLiveChild.actorLabels,
4783
- }
4784
- : taggedActorIdentity
4785
- ? {
4786
- ...cfg,
4787
- actorId: taggedActorIdentity.actorId,
4788
- actorType: taggedActorIdentity.actorType,
4789
- actorLabels: taggedActorIdentity.actorLabels,
4790
- }
4791
- : cfg;
4840
+ };
4841
+ shareRuntimePrivacyState(cfg, captureCfg);
4842
+ }
4843
+ else if (taggedActorIdentity) {
4844
+ captureCfg = {
4845
+ ...cfg,
4846
+ actorId: taggedActorIdentity.actorId,
4847
+ actorType: taggedActorIdentity.actorType,
4848
+ actorLabels: taggedActorIdentity.actorLabels,
4849
+ };
4850
+ shareRuntimePrivacyState(cfg, captureCfg);
4851
+ }
4792
4852
  if (taggedActorIdentity) {
4793
4853
  post(captureCfg, sid, {
4794
4854
  entries: [{
@@ -5262,17 +5322,17 @@ function reproMiddleware(cfg) {
5262
5322
  evt.__reproPending = {
5263
5323
  ...(ev.args !== undefined
5264
5324
  ? {
5265
- argsRaw: normalizeRawTraceArgs(ev.args),
5325
+ argsRaw: snapshotTraceArgsAtObservation(ev.args, captureScope),
5266
5326
  }
5267
5327
  : {}),
5268
5328
  ...(ev.returnValue !== undefined
5269
5329
  ? {
5270
- returnValueRaw: ev.returnValue,
5330
+ returnValueRaw: snapshotTraceValueAtObservation(ev.returnValue, captureScope, '$', 'trace.returnValue'),
5271
5331
  }
5272
5332
  : {}),
5273
5333
  ...(hasErrorPayload
5274
5334
  ? {
5275
- errorRaw: ev.error,
5335
+ errorRaw: snapshotTraceValueAtObservation(ev.error, captureScope, '$', 'trace.error'),
5276
5336
  }
5277
5337
  : {}),
5278
5338
  };
@@ -6621,6 +6681,7 @@ function recordKafkaTraceEvent(raw, sink, cfg, maskReq) {
6621
6681
  function buildPendingKafkaTraceEvent(raw) {
6622
6682
  if (!raw || (raw.type !== 'enter' && raw.type !== 'exit'))
6623
6683
  return null;
6684
+ raw = snapshotRawTraceEventAtObservation(raw, getCtx().captureScope);
6624
6685
  const sourceFileForLibrary = typeof raw.sourceFile === 'string' && raw.sourceFile
6625
6686
  ? String(raw.sourceFile)
6626
6687
  : null;
@@ -7054,7 +7115,7 @@ function wrapKafkaEachMessageHandler(eachMessage, cfg, consumer) {
7054
7115
  unsubscribe = __TRACER__.tracer.on((event) => {
7055
7116
  if (!event || event.traceId !== traceId)
7056
7117
  return;
7057
- rawTraceEvents.push(event);
7118
+ rawTraceEvents.push(snapshotRawTraceEventAtObservation(event, captureScope));
7058
7119
  });
7059
7120
  }
7060
7121
  catch { }
@@ -7168,7 +7229,7 @@ function wrapKafkaEachBatchHandler(eachBatch, cfg, consumer) {
7168
7229
  unsubscribe = __TRACER__.tracer.on((event) => {
7169
7230
  if (!event || event.traceId !== traceId)
7170
7231
  return;
7171
- rawTraceEvents.push(event);
7232
+ rawTraceEvents.push(snapshotRawTraceEventAtObservation(event, captureScope));
7172
7233
  });
7173
7234
  }
7174
7235
  catch { }
@@ -7886,7 +7947,7 @@ function wrapAmqplibConsumeHandler(onMessage, cfg, queue) {
7886
7947
  unsubscribe = __TRACER__.tracer.on((event) => {
7887
7948
  if (!event || event.traceId !== traceId)
7888
7949
  return;
7889
- rawTraceEvents.push(event);
7950
+ rawTraceEvents.push(snapshotRawTraceEventAtObservation(event, captureScope));
7890
7951
  });
7891
7952
  }
7892
7953
  catch { }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reproapp/node-sdk",
3
- "version": "0.0.48",
3
+ "version": "0.0.49",
4
4
  "description": "Repro Nest SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,7 +12,7 @@
12
12
  "build": "tsc -p tsconfig.json",
13
13
  "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
14
14
  "prepublishOnly": "npm run build",
15
- "test": "npm run build && node test/semantic-serialization.test.js && node test/semantic-serialization-trace.test.js && node test/content-addressed-trace-values.test.js && node test/session-storage-budget.test.js && node test/trace-source-exclude.test.js && node test/unawaited.test.js && node test/integration-unawaited.js && node test/request-flush-timing.test.js && node test/request-capture-background.test.js && node test/init-auto-http-server.test.js && node test/backend-live-mode.test.js && node test/client-performance-regression.test.js && node test/request-manifest-priority.test.js && node test/runtime-no-double-invoke.test.js && node test/express-trace-http-args.test.js && node test/trace-batch-size.test.js && node test/sdk-background-priority.test.js && node test/sdk-background-timeout-isolation.test.js && node test/runtime-service-registration.test.js && node test/ingest-queue-retention.test.js && node test/ingest-high-priority-retention.test.js && node test/ingest-high-priority-fairness.test.js && node test/ingest-critical-retention.test.js && node test/ingest-critical-retry-retention.test.js && node test/ingest-batching.test.js && node test/ingest-timeout.test.js && node -r ./tracer/register test/promise-map.test.js && node test/disable-subtree.test.js && node test/circular-capture.test.js && node test/wrap-plugin-arrow-args.test.js && node test/wrap-plugin-super-call.test.js && node test/function-arg-identity.test.js && node test/privacy-service-scope.test.js && node test/privacy-policy-index.test.js && node test/policy-capture-attestation.test.js && node test/privacy-runtime-policy.test.js && node test/runtime-privacy-materialization.test.js && node test/kafka-runtime-privacy-policy.test.js && node test/amqplib-integration.test.js && node test/mongodb-integration.test.js && node test/mongodb-esm-import-hook.test.js && node test/mongodb-suppression-cleanup.test.js && node test/postgresql-integration.test.js",
15
+ "test": "npm run build && node test/semantic-serialization.test.js && node test/semantic-serialization-trace.test.js && node test/content-addressed-trace-values.test.js && node test/trace-observation-snapshots.test.js && node test/session-storage-budget.test.js && node test/trace-source-exclude.test.js && node test/unawaited.test.js && node test/integration-unawaited.js && node test/request-flush-timing.test.js && node test/request-capture-background.test.js && node test/init-auto-http-server.test.js && node test/backend-live-mode.test.js && node test/client-performance-regression.test.js && node test/request-manifest-priority.test.js && node test/runtime-no-double-invoke.test.js && node test/express-trace-http-args.test.js && node test/trace-batch-size.test.js && node test/sdk-background-priority.test.js && node test/sdk-background-timeout-isolation.test.js && node test/runtime-service-registration.test.js && node test/ingest-queue-retention.test.js && node test/ingest-high-priority-retention.test.js && node test/ingest-high-priority-fairness.test.js && node test/ingest-critical-retention.test.js && node test/ingest-critical-retry-retention.test.js && node test/ingest-batching.test.js && node test/ingest-timeout.test.js && node -r ./tracer/register test/promise-map.test.js && node test/disable-subtree.test.js && node test/circular-capture.test.js && node test/wrap-plugin-arrow-args.test.js && node test/wrap-plugin-super-call.test.js && node test/function-arg-identity.test.js && node test/privacy-service-scope.test.js && node test/privacy-policy-index.test.js && node test/policy-capture-attestation.test.js && node test/privacy-runtime-policy.test.js && node test/runtime-privacy-materialization.test.js && node test/kafka-runtime-privacy-policy.test.js && node test/amqplib-integration.test.js && node test/mongodb-integration.test.js && node test/mongodb-esm-import-hook.test.js && node test/mongodb-suppression-cleanup.test.js && node test/postgresql-integration.test.js",
16
16
  "test:containers": "npm run build && node test/runtime-no-double-invoke.testcontainers.test.js && node test/wrap-plugin-super-call.testcontainers.test.js && node test/function-arg-identity.testcontainers.test.js && node test/mongodb-mongoose-native-suppression.testcontainers.test.js && node test/content-addressed-mongodb.testcontainers.test.js && node test/policy-session-completion.testcontainers.test.js && node test/postgresql-integrations.testcontainers.test.js"
17
17
  },
18
18
  "peerDependencies": {
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@ import type { Request, Response, NextFunction } from 'express';
2
2
  import type { Schema, Model, Query } from 'mongoose';
3
3
  import * as mongoose from 'mongoose';
4
4
  import { AsyncLocalStorage } from 'async_hooks';
5
- import { createHmac, randomUUID } from 'crypto';
5
+ import { createHash, createHmac, randomUUID } from 'crypto';
6
6
  import { createRequire } from 'module';
7
7
  import { join } from 'path';
8
8
  import { pathToFileURL } from 'url';
@@ -1228,6 +1228,7 @@ type RequestCaptureScopeState = {
1228
1228
  privacyPolicyFingerprint?: string;
1229
1229
  privacyStatementCount: number;
1230
1230
  emittedValueDefinitionIds: Set<string>;
1231
+ traceObservationSnapshots: Map<string, any>;
1231
1232
  };
1232
1233
 
1233
1234
  function createRequestCaptureScope(
@@ -1256,6 +1257,7 @@ function createRequestCaptureScope(
1256
1257
  privacyPolicyFingerprint: privacy?.fingerprint,
1257
1258
  privacyStatementCount: privacy?.statements.length ?? 0,
1258
1259
  emittedValueDefinitionIds: new Set(),
1260
+ traceObservationSnapshots: new Map(),
1259
1261
  };
1260
1262
  }
1261
1263
 
@@ -3403,6 +3405,107 @@ function hasMeaningfulRawTraceError(error: any): boolean {
3403
3405
  return error !== undefined && error !== null;
3404
3406
  }
3405
3407
 
3408
+ const TRACE_OBSERVATION_SNAPSHOT = Symbol('repro.traceObservationSnapshot');
3409
+
3410
+ // Trace events can remain queued after user code mutates the observed objects.
3411
+ // Intern immutable semantic snapshots per request so temporal evidence stays
3412
+ // correct without retaining another full copy for repeated unchanged values.
3413
+ function internTraceObservationSnapshot(
3414
+ snapshot: any,
3415
+ captureScope?: RequestCaptureScopeState | null,
3416
+ ): any {
3417
+ if (!captureScope || snapshot === null || typeof snapshot !== 'object') {
3418
+ return snapshot;
3419
+ }
3420
+
3421
+ const serialized = stableSerializeTraceValue(snapshot);
3422
+ if (serialized === null) return snapshot;
3423
+
3424
+ const digest = createHash('sha256')
3425
+ .update('repro-trace-observation-v1\0')
3426
+ .update(serialized)
3427
+ .digest('hex');
3428
+ const existing = captureScope.traceObservationSnapshots.get(digest);
3429
+ if (existing !== undefined) return existing;
3430
+
3431
+ captureScope.traceObservationSnapshots.set(digest, snapshot);
3432
+ return snapshot;
3433
+ }
3434
+
3435
+ function snapshotTraceValueAtObservation(
3436
+ value: any,
3437
+ captureScope?: RequestCaptureScopeState | null,
3438
+ valuePath = '$',
3439
+ target = 'trace.value',
3440
+ ): any {
3441
+ try {
3442
+ const snapshot = sanitizeTraceValue(
3443
+ value,
3444
+ 0,
3445
+ new WeakMap(),
3446
+ { preserveLongStrings: true, disableTruncation: true },
3447
+ valuePath,
3448
+ );
3449
+ return internTraceObservationSnapshot(snapshot, captureScope);
3450
+ } catch {
3451
+ recordRequestCaptureFailure(
3452
+ captureScope,
3453
+ 'trace.snapshot',
3454
+ target,
3455
+ 'snapshot_failed',
3456
+ );
3457
+ return makeOmittedValue('capture-snapshot-failed');
3458
+ }
3459
+ }
3460
+
3461
+ function snapshotTraceArgsAtObservation(
3462
+ values: any,
3463
+ captureScope?: RequestCaptureScopeState | null,
3464
+ ): any[] {
3465
+ const snapshot = snapshotTraceValueAtObservation(
3466
+ normalizeRawTraceArgs(values),
3467
+ captureScope,
3468
+ '$',
3469
+ 'trace.args',
3470
+ );
3471
+ return Array.isArray(snapshot) ? snapshot : [snapshot];
3472
+ }
3473
+
3474
+ function snapshotRawTraceEventAtObservation(
3475
+ raw: any,
3476
+ captureScope?: RequestCaptureScopeState | null,
3477
+ ): any {
3478
+ if (!raw || typeof raw !== 'object' || raw[TRACE_OBSERVATION_SNAPSHOT] === true) {
3479
+ return raw;
3480
+ }
3481
+
3482
+ const snapshot = { ...raw };
3483
+ if (raw.args !== undefined) {
3484
+ snapshot.args = snapshotTraceArgsAtObservation(raw.args, captureScope);
3485
+ }
3486
+ if (raw.returnValue !== undefined) {
3487
+ snapshot.returnValue = snapshotTraceValueAtObservation(
3488
+ raw.returnValue,
3489
+ captureScope,
3490
+ '$',
3491
+ 'trace.returnValue',
3492
+ );
3493
+ }
3494
+ if (hasMeaningfulRawTraceError(raw.error)) {
3495
+ snapshot.error = snapshotTraceValueAtObservation(
3496
+ raw.error,
3497
+ captureScope,
3498
+ '$',
3499
+ 'trace.error',
3500
+ );
3501
+ }
3502
+ Object.defineProperty(snapshot, TRACE_OBSERVATION_SNAPSHOT, {
3503
+ value: true,
3504
+ enumerable: false,
3505
+ });
3506
+ return snapshot;
3507
+ }
3508
+
3406
3509
  function attachTraceValueEntry(
3407
3510
  event: TraceEventRecord | PendingTraceEventRecord,
3408
3511
  entry: TraceValueBatchEntry,
@@ -6046,21 +6149,24 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6046
6149
  res.on('close', endTrackedClientRequest);
6047
6150
  return next(); // only capture tagged requests
6048
6151
  }
6049
- const captureCfg: ReproMiddlewareConfig = backendLiveChild
6050
- ? {
6152
+ let captureCfg: ReproMiddlewareConfig = cfg;
6153
+ if (backendLiveChild) {
6154
+ captureCfg = {
6051
6155
  ...cfg,
6052
6156
  actorId: backendLiveChild.actorId,
6053
6157
  actorType: backendLiveChild.actorType,
6054
6158
  actorLabels: backendLiveChild.actorLabels,
6055
- }
6056
- : taggedActorIdentity
6057
- ? {
6058
- ...cfg,
6059
- actorId: taggedActorIdentity.actorId,
6060
- actorType: taggedActorIdentity.actorType,
6061
- actorLabels: taggedActorIdentity.actorLabels,
6062
- }
6063
- : cfg;
6159
+ };
6160
+ shareRuntimePrivacyState(cfg, captureCfg);
6161
+ } else if (taggedActorIdentity) {
6162
+ captureCfg = {
6163
+ ...cfg,
6164
+ actorId: taggedActorIdentity.actorId,
6165
+ actorType: taggedActorIdentity.actorType,
6166
+ actorLabels: taggedActorIdentity.actorLabels,
6167
+ };
6168
+ shareRuntimePrivacyState(cfg, captureCfg);
6169
+ }
6064
6170
  if (taggedActorIdentity) {
6065
6171
  post(captureCfg, sid, {
6066
6172
  entries: [{
@@ -6588,17 +6694,27 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6588
6694
  evt.__reproPending = {
6589
6695
  ...(ev.args !== undefined
6590
6696
  ? {
6591
- argsRaw: normalizeRawTraceArgs(ev.args),
6697
+ argsRaw: snapshotTraceArgsAtObservation(ev.args, captureScope),
6592
6698
  }
6593
6699
  : {}),
6594
6700
  ...(ev.returnValue !== undefined
6595
6701
  ? {
6596
- returnValueRaw: ev.returnValue,
6702
+ returnValueRaw: snapshotTraceValueAtObservation(
6703
+ ev.returnValue,
6704
+ captureScope,
6705
+ '$',
6706
+ 'trace.returnValue',
6707
+ ),
6597
6708
  }
6598
6709
  : {}),
6599
6710
  ...(hasErrorPayload
6600
6711
  ? {
6601
- errorRaw: ev.error,
6712
+ errorRaw: snapshotTraceValueAtObservation(
6713
+ ev.error,
6714
+ captureScope,
6715
+ '$',
6716
+ 'trace.error',
6717
+ ),
6602
6718
  }
6603
6719
  : {}),
6604
6720
  };
@@ -8094,6 +8210,8 @@ function recordKafkaTraceEvent(
8094
8210
  function buildPendingKafkaTraceEvent(raw: any): PendingTraceEventRecord | null {
8095
8211
  if (!raw || (raw.type !== 'enter' && raw.type !== 'exit')) return null;
8096
8212
 
8213
+ raw = snapshotRawTraceEventAtObservation(raw, getCtx().captureScope);
8214
+
8097
8215
  const sourceFileForLibrary =
8098
8216
  typeof raw.sourceFile === 'string' && raw.sourceFile
8099
8217
  ? String(raw.sourceFile)
@@ -8574,7 +8692,7 @@ function wrapKafkaEachMessageHandler(eachMessage: any, cfg: KafkaJsPatchConfig,
8574
8692
  try {
8575
8693
  unsubscribe = __TRACER__.tracer.on((event: any) => {
8576
8694
  if (!event || event.traceId !== traceId) return;
8577
- rawTraceEvents.push(event);
8695
+ rawTraceEvents.push(snapshotRawTraceEventAtObservation(event, captureScope));
8578
8696
  });
8579
8697
  } catch {}
8580
8698
  }
@@ -8684,7 +8802,7 @@ function wrapKafkaEachBatchHandler(eachBatch: any, cfg: KafkaJsPatchConfig, cons
8684
8802
  try {
8685
8803
  unsubscribe = __TRACER__.tracer.on((event: any) => {
8686
8804
  if (!event || event.traceId !== traceId) return;
8687
- rawTraceEvents.push(event);
8805
+ rawTraceEvents.push(snapshotRawTraceEventAtObservation(event, captureScope));
8688
8806
  });
8689
8807
  } catch {}
8690
8808
  }
@@ -9492,7 +9610,7 @@ function wrapAmqplibConsumeHandler(onMessage: any, cfg: AmqplibPatchConfig, queu
9492
9610
  try {
9493
9611
  unsubscribe = __TRACER__.tracer.on((event: any) => {
9494
9612
  if (!event || event.traceId !== traceId) return;
9495
- rawTraceEvents.push(event);
9613
+ rawTraceEvents.push(snapshotRawTraceEventAtObservation(event, captureScope));
9496
9614
  });
9497
9615
  } catch {}
9498
9616
  };
@@ -215,6 +215,7 @@ async function main() {
215
215
  const middleware = reproMiddleware(config);
216
216
  const { req, res } = makeReqRes('S_content_addressed_mongo', 'A_content_addressed_mongo');
217
217
  const repository = new SubjectRepository(Subject, new ConfigService());
218
+ const queryInput = { subjectNumber: 'SUBJECT-CONTENT-1001' };
218
219
  let operationResult;
219
220
 
220
221
  await new Promise((resolve, reject) => {
@@ -227,12 +228,14 @@ async function main() {
227
228
  operationResult = await global.__repro_call(
228
229
  loadSubjectRepeatedly,
229
230
  null,
230
- [repository, { subjectNumber: 'SUBJECT-CONTENT-1001' }],
231
+ [repository, queryInput],
231
232
  '/app/src/subjects.repository.ts',
232
233
  120,
233
234
  'loadSubjectRepeatedly',
234
235
  false,
235
236
  );
237
+ queryInput.subjectNumber = 'MUTATED-AFTER-TRACE-EXIT';
238
+ operationResult.docs[0].status = 'mutated-after-trace-exit';
236
239
  res.end();
237
240
  resolve();
238
241
  } catch (operationError) {
@@ -313,15 +316,31 @@ async function main() {
313
316
  const argsDefinition = traceDefinitions.find((entry) =>
314
317
  entry.id === enter.argsValueCapture?.valueId
315
318
  );
316
- assert(argsDefinition, JSON.stringify(traceDefinitions));
317
- assert.deepEqual(argsDefinition.value[0].configService, {
319
+ const capturedArgs = argsDefinition?.value ?? enter.args;
320
+ assert(Array.isArray(capturedArgs), JSON.stringify(enter));
321
+ assert.deepEqual(capturedArgs[0].configService, {
318
322
  __kind: 'repro-semantic',
319
323
  category: 'configuration-service',
320
324
  className: 'ConfigService',
321
325
  });
322
- assert.equal(argsDefinition.value[1].subjectNumber, 'SUBJECT-CONTENT-1001');
326
+ assert.equal(capturedArgs[1].subjectNumber, 'SUBJECT-CONTENT-1001');
327
+
328
+ const exit = traceEvents.find((event) =>
329
+ event.fn === 'loadSubjectRepeatedly' && event.type === 'exit'
330
+ );
331
+ assert(exit, JSON.stringify(traceEvents));
332
+ const returnDefinition = traceDefinitions.find((entry) =>
333
+ entry.id === exit.returnValueCapture?.valueId
334
+ );
335
+ assert(returnDefinition, JSON.stringify({
336
+ exit,
337
+ definitions: traceDefinitions.map((entry) => ({ id: entry.id, target: entry.target })),
338
+ }));
339
+ assert.equal(returnDefinition.value.docs[0].status, 'screened');
323
340
 
324
341
  const capturedJson = JSON.stringify(posts);
342
+ assert(!capturedJson.includes('MUTATED-AFTER-TRACE-EXIT'));
343
+ assert(!capturedJson.includes('mutated-after-trace-exit'));
325
344
  assert(!capturedJson.includes('captured-config-secret'));
326
345
  assert(!capturedJson.includes('mongo-config-credential-999'));
327
346
  assert(!capturedJson.includes('mongo-test-logger-buffer'));
@@ -145,10 +145,19 @@ async function testKafkaConsumerTraceUsesConsumeTraceId() {
145
145
  privacy: { environment: 'dev' },
146
146
  ingestBase: 'http://127.0.0.1:65535',
147
147
  };
148
+ const consumerPayload = {
149
+ marker: 'before-consumer-work',
150
+ repeated: 'consumer-trace-value'.repeat(1024),
151
+ };
148
152
  const wrapped = __reproTestHooks.wrapKafkaEachMessageHandlerForTest(
149
153
  async () => {
150
- tracer.tracer.enter('consumerWork', { file: '/app/consumer.ts', line: 10 }, { args: ['ok'] });
151
- tracer.tracer.exit({ fn: 'consumerWork', file: '/app/consumer.ts', line: 10 }, { returnValue: 'done' });
154
+ tracer.tracer.enter('consumerWork', { file: '/app/consumer.ts', line: 10 }, { args: [consumerPayload] });
155
+ consumerPayload.marker = 'at-consumer-exit';
156
+ tracer.tracer.exit(
157
+ { fn: 'consumerWork', file: '/app/consumer.ts', line: 10 },
158
+ { args: [consumerPayload], returnValue: consumerPayload },
159
+ );
160
+ consumerPayload.marker = 'after-consumer-exit';
152
161
 
153
162
  tracer.withTrace('parent-http-trace', () => {
154
163
  tracer.tracer.enter('parentWork', { file: '/app/controller.ts', line: 20 }, { args: ['wrong'] });
@@ -207,6 +216,21 @@ async function testKafkaConsumerTraceUsesConsumeTraceId() {
207
216
  .map((traceEvent) => traceEvent.fn);
208
217
  assert(traceFns.includes('consumerWork'), traceFns.join(','));
209
218
  assert(!traceFns.includes('parentWork'), traceFns.join(','));
219
+
220
+ const traceBatches = events.filter((event) => event.event_type === 'trace_batch');
221
+ const traceEvents = traceBatches.flatMap((event) => event.payload?.trace || []);
222
+ const definitions = traceBatches.flatMap((event) => event.payload?.traceValues || []);
223
+ const definitionsById = new Map(definitions.map((definition) => [definition.id, definition]));
224
+ const consumerEnter = traceEvents.find((event) => event.fn === 'consumerWork' && event.type === 'enter');
225
+ const consumerExit = traceEvents.find((event) => event.fn === 'consumerWork' && event.type === 'exit');
226
+ assert(consumerEnter && consumerExit, JSON.stringify(traceEvents));
227
+ const enterValue = definitionsById.get(consumerEnter.argsValueCapture?.valueId)?.value;
228
+ const exitArgsValue = definitionsById.get(consumerExit.argsValueCapture?.valueId)?.value;
229
+ const returnValue = definitionsById.get(consumerExit.returnValueCapture?.valueId)?.value;
230
+ assert.equal(enterValue?.[0]?.marker, 'before-consumer-work');
231
+ assert.equal(exitArgsValue?.[0]?.marker, 'at-consumer-exit');
232
+ assert.equal(returnValue?.marker, 'at-consumer-exit');
233
+ assert(!JSON.stringify(definitions).includes('after-consumer-exit'));
210
234
  } finally {
211
235
  global.fetch = originalFetch;
212
236
  }
@@ -79,6 +79,7 @@ function requestNames(port, index) {
79
79
  'x-bug-session-id': 'S_policy_completion',
80
80
  'x-bug-action-id': `A_policy_completion_${index}`,
81
81
  'x-bug-request-start': String(Date.now()),
82
+ 'x-demo-actor-id': 'policy-completion-actor',
82
83
  },
83
84
  }, (response) => {
84
85
  const chunks = [];
@@ -152,10 +153,26 @@ async function main() {
152
153
  captureHeaders: false,
153
154
  privacy: {
154
155
  environment: 'dev',
155
- policy: largePolicy(),
156
+ },
157
+ liveMode: {
158
+ enabled: true,
159
+ backendOnly: true,
160
+ defineActorId: ({ req }) => req.headers['x-demo-actor-id'],
161
+ shouldCapture: () => true,
162
+ actorType: 'policy-test-user',
156
163
  },
157
164
  ingestBase: 'http://127.0.0.1:65535',
158
165
  };
166
+ __reproTestHooks.shareRuntimePrivacyStateForTest(
167
+ {
168
+ ...cfg,
169
+ privacy: {
170
+ environment: 'dev',
171
+ policy: largePolicy(),
172
+ },
173
+ },
174
+ cfg,
175
+ );
159
176
 
160
177
  const depotSchema = new mongoose.Schema({
161
178
  location: String,
@@ -0,0 +1,403 @@
1
+ process.env.REPRO_SDK_BACKGROUND_MAX_DEFER_MS = '50';
2
+ process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '0';
3
+ process.env.REPRO_SDK_BACKGROUND_BUSY_IDLE_DELAY_MS = '0';
4
+ process.env.REPRO_SDK_TRACE_INLINE_VALUE_MAX_SERIALIZED_CHARS = '256';
5
+ process.env.REPRO_TRACE_BATCH_MAX_SERIALIZED_BYTES = '65536';
6
+ process.env.TRACE_IDLE_FLUSH_MS = '10';
7
+ process.env.TRACE_LINGER_AFTER_FINISH_MS = '0';
8
+
9
+ const assert = require('node:assert/strict');
10
+ const { EventEmitter } = require('node:events');
11
+ const { initReproTracing, reproMiddleware, __reproTestHooks } = require('../dist');
12
+ const { flushIngestQueue } = require('../dist/ingest/client');
13
+
14
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
15
+
16
+ function deferred() {
17
+ let resolve;
18
+ let reject;
19
+ const promise = new Promise((resolvePromise, rejectPromise) => {
20
+ resolve = resolvePromise;
21
+ reject = rejectPromise;
22
+ });
23
+ return { promise, resolve, reject };
24
+ }
25
+
26
+ function makeReqRes(sessionId) {
27
+ const req = new EventEmitter();
28
+ req.method = 'POST';
29
+ req.url = '/trace-observation-snapshots';
30
+ req.originalUrl = req.url;
31
+ req.headers = {
32
+ 'x-bug-session-id': sessionId,
33
+ 'x-bug-action-id': `A_${sessionId}`,
34
+ 'x-bug-request-start': String(Date.now()),
35
+ };
36
+ req.body = {};
37
+ req.params = {};
38
+ req.query = {};
39
+
40
+ const res = new EventEmitter();
41
+ res.statusCode = 204;
42
+ res.getHeader = () => undefined;
43
+ res.getHeaders = () => ({});
44
+ res.setHeader = () => {};
45
+ res.write = () => true;
46
+ res.end = function end() {
47
+ this.emit('finish');
48
+ return this;
49
+ };
50
+ return { req, res };
51
+ }
52
+
53
+ function makePayload(id, phase = 'before') {
54
+ return {
55
+ id,
56
+ phase,
57
+ nested: { count: 0 },
58
+ rows: Array.from({ length: 20 }, (_, index) => ({
59
+ index,
60
+ location: `${id}-LOCATION-${String(index).padStart(3, '0')}`,
61
+ state: 'initial',
62
+ })),
63
+ };
64
+ }
65
+
66
+ function mutateSyncPayload(payload) {
67
+ payload.phase = 'inside-sync';
68
+ payload.nested.count = 1;
69
+ payload.rows[0].state = 'inside-sync';
70
+ return payload;
71
+ }
72
+
73
+ function observeVersion(payload) {
74
+ return payload.phase;
75
+ }
76
+
77
+ function observeAliasedPayload(payload) {
78
+ return payload;
79
+ }
80
+
81
+ async function mutateAsyncPayload(payload, gate) {
82
+ payload.phase = 'started';
83
+ await gate;
84
+ payload.phase = 'settled';
85
+ payload.nested.count += 1;
86
+ payload.rows[0].state = 'settled';
87
+ return payload;
88
+ }
89
+
90
+ function mutateAndThrow(payload) {
91
+ payload.phase = 'before-throw';
92
+ payload.nested.count = 7;
93
+ throw new Error('expected trace snapshot failure path');
94
+ }
95
+
96
+ function observeHostilePayload(payload) {
97
+ return payload;
98
+ }
99
+
100
+ async function waitForCapture(posts) {
101
+ const deadline = Date.now() + 5000;
102
+ while (Date.now() < deadline) {
103
+ const events = posts.flatMap((post) => post.events || []);
104
+ if (events.some((event) => event.event_type === 'backend_request') &&
105
+ events.some((event) => event.event_type === 'trace_batch')) {
106
+ break;
107
+ }
108
+ await sleep(20);
109
+ }
110
+ await __reproTestHooks.drainSdkBackgroundQueueForTest();
111
+ await flushIngestQueue();
112
+ }
113
+
114
+ function inspectCapture(posts) {
115
+ const ingestEvents = posts.flatMap((post) => post.events || []);
116
+ const traceBatches = ingestEvents.filter((event) => event.event_type === 'trace_batch');
117
+ const events = traceBatches.flatMap((event) => event.payload?.trace || []);
118
+ const definitions = traceBatches.flatMap((event) => event.payload?.traceValues || []);
119
+ const definitionsById = new Map(definitions.map((definition) => [definition.id, definition]));
120
+
121
+ const resolveCaptured = (event, field) => {
122
+ const capture = event[field];
123
+ assert.equal(capture?.status, 'stored', `${event.fn} ${event.type} lacks ${field}`);
124
+ const definition = definitionsById.get(capture.valueId);
125
+ assert(definition, `missing definition ${capture.valueId}`);
126
+ return definition.value;
127
+ };
128
+
129
+ return { ingestEvents, traceBatches, events, definitions, definitionsById, resolveCaptured };
130
+ }
131
+
132
+ function eventsFor(capture, fn) {
133
+ return capture.events.filter((event) => event.fn === fn);
134
+ }
135
+
136
+ async function main() {
137
+ const config = {
138
+ tenantId: 'TENANT_trace_observation_snapshot',
139
+ appId: 'APP_trace_observation_snapshot',
140
+ appSecret: 'trace-observation-snapshot-secret',
141
+ captureHeaders: false,
142
+ privacy: { environment: 'dev', captureRaw: true },
143
+ ingestBase: 'http://127.0.0.1:65535',
144
+ };
145
+
146
+ initReproTracing({ instrument: false, fullValueCapture: true, logFunctionCalls: false });
147
+ __reproTestHooks.resetSdkBackgroundQueuesForTest();
148
+
149
+ const posts = [];
150
+ const previousFetch = global.fetch;
151
+ global.fetch = async (_url, init = {}) => {
152
+ posts.push(JSON.parse(String(init.body || '{}')));
153
+ return {
154
+ ok: true,
155
+ status: 202,
156
+ json: async () => ({ ok: true }),
157
+ text: async () => '{"ok":true}',
158
+ };
159
+ };
160
+
161
+ const sessionId = 'S_trace_observation_snapshot';
162
+ const middleware = reproMiddleware(config);
163
+ const { req, res } = makeReqRes(sessionId);
164
+
165
+ try {
166
+ await new Promise((resolve, reject) => {
167
+ middleware(req, res, async (middlewareError) => {
168
+ if (middlewareError) {
169
+ reject(middlewareError);
170
+ return;
171
+ }
172
+
173
+ try {
174
+ const syncPayload = makePayload('sync');
175
+ const syncResult = global.__repro_call(
176
+ mutateSyncPayload,
177
+ null,
178
+ [syncPayload],
179
+ '/app/trace-observation.service.ts',
180
+ 20,
181
+ 'mutateSyncPayload',
182
+ false,
183
+ );
184
+ assert.strictEqual(syncResult, syncPayload, 'tracing changed sync return identity');
185
+ syncPayload.phase = 'after-sync-caller';
186
+ syncPayload.nested.count = 99;
187
+ syncPayload.rows[0].state = 'after-sync-caller';
188
+
189
+ const versionedPayload = makePayload('versioned', 'v1');
190
+ for (const phase of ['v1', 'v2', 'v1']) {
191
+ versionedPayload.phase = phase;
192
+ const observed = global.__repro_call(
193
+ observeVersion,
194
+ null,
195
+ [versionedPayload],
196
+ '/app/trace-observation.service.ts',
197
+ 40,
198
+ 'observeVersion',
199
+ false,
200
+ );
201
+ assert.equal(observed, phase);
202
+ }
203
+ versionedPayload.phase = 'after-version-caller';
204
+
205
+ const shared = {
206
+ value: 'alias-before',
207
+ rows: makePayload('alias-rows').rows,
208
+ };
209
+ const aliasedPayload = { id: 'alias', left: shared, right: shared };
210
+ aliasedPayload.self = aliasedPayload;
211
+ const aliasResult = global.__repro_call(
212
+ observeAliasedPayload,
213
+ null,
214
+ [aliasedPayload],
215
+ '/app/trace-observation.service.ts',
216
+ 60,
217
+ 'observeAliasedPayload',
218
+ false,
219
+ );
220
+ assert.strictEqual(aliasResult, aliasedPayload, 'tracing changed aliased return identity');
221
+ shared.value = 'alias-after-caller';
222
+
223
+ const gateA = deferred();
224
+ const gateB = deferred();
225
+ const asyncPayloadA = makePayload('async-a');
226
+ const asyncPayloadB = makePayload('async-b');
227
+ const pendingA = global.__repro_call(
228
+ mutateAsyncPayload,
229
+ null,
230
+ [asyncPayloadA, gateA.promise],
231
+ '/app/trace-observation.service.ts',
232
+ 80,
233
+ 'mutateAsyncPayloadA',
234
+ false,
235
+ );
236
+ const pendingB = global.__repro_call(
237
+ mutateAsyncPayload,
238
+ null,
239
+ [asyncPayloadB, gateB.promise],
240
+ '/app/trace-observation.service.ts',
241
+ 90,
242
+ 'mutateAsyncPayloadB',
243
+ false,
244
+ );
245
+ gateB.resolve();
246
+ assert.strictEqual(await pendingB, asyncPayloadB);
247
+ asyncPayloadB.phase = 'after-async-b-caller';
248
+ asyncPayloadB.nested.count = 99;
249
+ gateA.resolve();
250
+ assert.strictEqual(await pendingA, asyncPayloadA);
251
+ asyncPayloadA.phase = 'after-async-a-caller';
252
+ asyncPayloadA.nested.count = 99;
253
+
254
+ const throwingPayload = makePayload('throwing');
255
+ assert.throws(() => global.__repro_call(
256
+ mutateAndThrow,
257
+ null,
258
+ [throwingPayload],
259
+ '/app/trace-observation.service.ts',
260
+ 110,
261
+ 'mutateAndThrow',
262
+ false,
263
+ ), /expected trace snapshot failure path/);
264
+ throwingPayload.phase = 'after-throw-caller';
265
+ throwingPayload.nested.count = 99;
266
+
267
+ const hostilePayload = new Proxy(
268
+ { id: 'hostile-payload' },
269
+ {
270
+ ownKeys() {
271
+ throw new Error('hostile ownKeys trap');
272
+ },
273
+ },
274
+ );
275
+ const hostileResult = global.__repro_call(
276
+ observeHostilePayload,
277
+ null,
278
+ [hostilePayload],
279
+ '/app/trace-observation.service.ts',
280
+ 130,
281
+ 'observeHostilePayload',
282
+ false,
283
+ );
284
+ assert.strictEqual(hostileResult, hostilePayload, 'snapshot failure changed application identity');
285
+
286
+ res.end();
287
+ resolve();
288
+ } catch (error) {
289
+ reject(error);
290
+ }
291
+ });
292
+ });
293
+
294
+ await waitForCapture(posts);
295
+ const capture = inspectCapture(posts);
296
+
297
+ const syncEvents = eventsFor(capture, 'mutateSyncPayload');
298
+ assert.equal(syncEvents.length, 2);
299
+ const syncEnterArgs = capture.resolveCaptured(syncEvents.find((event) => event.type === 'enter'), 'argsValueCapture');
300
+ const syncExit = syncEvents.find((event) => event.type === 'exit');
301
+ const syncExitArgs = capture.resolveCaptured(syncExit, 'argsValueCapture');
302
+ const syncReturn = capture.resolveCaptured(syncExit, 'returnValueCapture');
303
+ assert.deepEqual(
304
+ [syncEnterArgs[0].phase, syncEnterArgs[0].nested.count, syncEnterArgs[0].rows[0].state],
305
+ ['before', 0, 'initial'],
306
+ );
307
+ assert.deepEqual(
308
+ [syncExitArgs[0].phase, syncExitArgs[0].nested.count, syncExitArgs[0].rows[0].state],
309
+ ['inside-sync', 1, 'inside-sync'],
310
+ );
311
+ assert.deepEqual(
312
+ [syncReturn.phase, syncReturn.nested.count, syncReturn.rows[0].state],
313
+ ['inside-sync', 1, 'inside-sync'],
314
+ );
315
+
316
+ const versionEvents = eventsFor(capture, 'observeVersion');
317
+ assert.equal(versionEvents.length, 6);
318
+ const versionRefs = versionEvents.map((event) => event.argsValueCapture?.valueId);
319
+ assert(versionRefs.every((valueId) => typeof valueId === 'string'));
320
+ assert.equal(versionRefs[0], versionRefs[1], 'unchanged enter/exit observations must deduplicate');
321
+ assert.equal(versionRefs[2], versionRefs[3], 'second unchanged enter/exit observations must deduplicate');
322
+ assert.equal(versionRefs[4], versionRefs[5], 'restored enter/exit observations must deduplicate');
323
+ assert.equal(versionRefs[0], versionRefs[4], 'restoring prior content must restore its content ID');
324
+ assert.notEqual(versionRefs[0], versionRefs[2], 'same object with changed content needs a new content ID');
325
+ assert.deepEqual(
326
+ [0, 2, 4].map((index) => capture.resolveCaptured(versionEvents[index], 'argsValueCapture')[0].phase),
327
+ ['v1', 'v2', 'v1'],
328
+ );
329
+
330
+ const aliasEvents = eventsFor(capture, 'observeAliasedPayload');
331
+ assert.equal(aliasEvents.length, 2);
332
+ const aliasArgs = capture.resolveCaptured(aliasEvents[0], 'argsValueCapture');
333
+ assert.equal(aliasArgs[0].left.value, 'alias-before');
334
+ assert.deepEqual(aliasArgs[0].right, { __reproCircularRef: '$[0].left' });
335
+ assert.deepEqual(aliasArgs[0].self, { __reproCircularRef: '$[0]' });
336
+
337
+ for (const fn of ['mutateAsyncPayloadA', 'mutateAsyncPayloadB']) {
338
+ const asyncEvents = eventsFor(capture, fn);
339
+ assert.equal(asyncEvents.length, 2, `${fn} event count`);
340
+ const enterArgs = capture.resolveCaptured(asyncEvents.find((event) => event.type === 'enter'), 'argsValueCapture');
341
+ const exit = asyncEvents.find((event) => event.type === 'exit');
342
+ const exitArgs = capture.resolveCaptured(exit, 'argsValueCapture');
343
+ const returnValue = capture.resolveCaptured(exit, 'returnValueCapture');
344
+ assert.equal(enterArgs[0].phase, 'before', `${fn} enter was rewritten`);
345
+ assert.equal(enterArgs[0].nested.count, 0);
346
+ assert.equal(exitArgs[0].phase, 'settled', `${fn} exit was rewritten by caller mutation`);
347
+ assert.equal(exitArgs[0].nested.count, 1);
348
+ assert.equal(returnValue.phase, 'settled', `${fn} return was rewritten by caller mutation`);
349
+ assert.equal(returnValue.nested.count, 1);
350
+ }
351
+
352
+ const throwEvents = eventsFor(capture, 'mutateAndThrow');
353
+ assert.equal(throwEvents.length, 2);
354
+ const throwExit = throwEvents.find((event) => event.type === 'exit');
355
+ const throwArgs = capture.resolveCaptured(throwExit, 'argsValueCapture');
356
+ const throwError = capture.resolveCaptured(throwExit, 'errorValueCapture');
357
+ assert.equal(throwArgs[0].phase, 'before-throw');
358
+ assert.equal(throwArgs[0].nested.count, 7);
359
+ assert.equal(throwExit.threw, true);
360
+ assert.equal(throwError.message, 'expected trace snapshot failure path');
361
+
362
+ const hostileEvents = eventsFor(capture, 'observeHostilePayload');
363
+ assert.equal(hostileEvents.length, 2);
364
+ for (const hostileEvent of hostileEvents) {
365
+ assert.deepEqual(hostileEvent.args, [{
366
+ __kind: 'omitted',
367
+ reason: 'capture-snapshot-failed',
368
+ }]);
369
+ }
370
+ const manifests = capture.ingestEvents
371
+ .filter((event) => event.event_type === 'request_capture_manifest')
372
+ .map((event) => event.payload?.requestCaptureManifest ?? event.payload)
373
+ .filter(Boolean);
374
+ assert.equal(manifests.length, 1);
375
+ assert.equal(manifests[0].privacy?.status, 'failed');
376
+ assert(manifests[0].captureFailures?.some((failure) =>
377
+ failure.stage === 'trace.snapshot' &&
378
+ failure.target === 'trace.args' &&
379
+ failure.reason === 'snapshot_failed'
380
+ ), JSON.stringify(manifests[0]));
381
+
382
+ const serializedDefinitions = JSON.stringify(capture.definitions);
383
+ for (const forbidden of [
384
+ 'after-sync-caller',
385
+ 'after-version-caller',
386
+ 'alias-after-caller',
387
+ 'after-async-a-caller',
388
+ 'after-async-b-caller',
389
+ 'after-throw-caller',
390
+ ]) {
391
+ assert(!serializedDefinitions.includes(forbidden), `late mutation leaked into capture: ${forbidden}`);
392
+ }
393
+
394
+ console.log('trace observation snapshots preserve temporal values and application identity OK');
395
+ } finally {
396
+ global.fetch = previousFetch;
397
+ }
398
+ }
399
+
400
+ main().catch((error) => {
401
+ console.error(error);
402
+ process.exit(1);
403
+ });