@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 +1 -0
- package/dist/index.js +78 -17
- package/package.json +2 -2
- package/src/index.ts +136 -18
- package/test/content-addressed-mongodb.testcontainers.test.js +23 -4
- package/test/kafka-runtime-privacy-policy.test.js +26 -2
- package/test/policy-session-completion.testcontainers.test.js +18 -1
- package/test/trace-observation-snapshots.test.js +403 -0
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
|
-
|
|
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
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
:
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
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:
|
|
6697
|
+
argsRaw: snapshotTraceArgsAtObservation(ev.args, captureScope),
|
|
6592
6698
|
}
|
|
6593
6699
|
: {}),
|
|
6594
6700
|
...(ev.returnValue !== undefined
|
|
6595
6701
|
? {
|
|
6596
|
-
returnValueRaw:
|
|
6702
|
+
returnValueRaw: snapshotTraceValueAtObservation(
|
|
6703
|
+
ev.returnValue,
|
|
6704
|
+
captureScope,
|
|
6705
|
+
'$',
|
|
6706
|
+
'trace.returnValue',
|
|
6707
|
+
),
|
|
6597
6708
|
}
|
|
6598
6709
|
: {}),
|
|
6599
6710
|
...(hasErrorPayload
|
|
6600
6711
|
? {
|
|
6601
|
-
errorRaw:
|
|
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,
|
|
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
|
-
|
|
317
|
-
assert.
|
|
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(
|
|
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: [
|
|
151
|
-
|
|
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
|
-
|
|
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
|
+
});
|