@reproapp/node-sdk 0.0.54 → 0.0.56
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/capture-work-budget.js +1 -2
- package/dist/index.d.ts +1 -26
- package/dist/index.js +30 -24
- package/dist/ingest/client.d.ts +2 -2
- package/dist/integrations/sendgrid.js +2 -2
- package/dist/live-capture-limits.js +4 -4
- package/dist/privacy-fallback.js +1 -2
- package/dist/privacy-redaction.js +3 -4
- package/dist/privacy.js +7 -8
- package/dist/semantic-serialization.js +2 -3
- package/package.json +3 -1
- package/src/index.ts +13 -6
- package/test/amqplib-timing.testcontainers.test.js +198 -0
- package/test/capture-latency.testcontainers.test.js +3 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.yieldCaptureWork =
|
|
3
|
+
exports.yieldCaptureWork = yieldCaptureWork;
|
|
4
4
|
// Promise-only recursion never yields to HTTP sockets. Share a small time slice
|
|
5
5
|
// across asynchronous capture walkers, including the worker-unavailable fallback.
|
|
6
6
|
let deadline = 0;
|
|
@@ -22,4 +22,3 @@ function yieldCaptureWork() {
|
|
|
22
22
|
}));
|
|
23
23
|
return pendingYield;
|
|
24
24
|
}
|
|
25
|
-
exports.yieldCaptureWork = yieldCaptureWork;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,28 +1,3 @@
|
|
|
1
|
-
/// <reference types="mongoose/types/aggregate" />
|
|
2
|
-
/// <reference types="mongoose/types/callback" />
|
|
3
|
-
/// <reference types="mongoose/types/collection" />
|
|
4
|
-
/// <reference types="mongoose/types/connection" />
|
|
5
|
-
/// <reference types="mongoose/types/cursor" />
|
|
6
|
-
/// <reference types="mongoose/types/document" />
|
|
7
|
-
/// <reference types="mongoose/types/error" />
|
|
8
|
-
/// <reference types="mongoose/types/expressions" />
|
|
9
|
-
/// <reference types="mongoose/types/helpers" />
|
|
10
|
-
/// <reference types="mongoose/types/middlewares" />
|
|
11
|
-
/// <reference types="mongoose/types/indexes" />
|
|
12
|
-
/// <reference types="mongoose/types/models" />
|
|
13
|
-
/// <reference types="mongoose/types/mongooseoptions" />
|
|
14
|
-
/// <reference types="mongoose/types/pipelinestage" />
|
|
15
|
-
/// <reference types="mongoose/types/populate" />
|
|
16
|
-
/// <reference types="mongoose/types/query" />
|
|
17
|
-
/// <reference types="mongoose/types/schemaoptions" />
|
|
18
|
-
/// <reference types="mongoose/types/session" />
|
|
19
|
-
/// <reference types="mongoose/types/types" />
|
|
20
|
-
/// <reference types="mongoose/types/utility" />
|
|
21
|
-
/// <reference types="mongoose/types/validation" />
|
|
22
|
-
/// <reference types="mongoose/types/virtuals" />
|
|
23
|
-
/// <reference types="mongoose/types/schematypes" />
|
|
24
|
-
/// <reference types="mongoose/types/inferschematype" />
|
|
25
|
-
/// <reference types="mongoose/types/inferrawdoctype" />
|
|
26
1
|
import type { Request, Response, NextFunction } from 'express';
|
|
27
2
|
import type { Schema } from 'mongoose';
|
|
28
3
|
import { type IngestClientConfig } from './ingest/client';
|
|
@@ -485,7 +460,7 @@ export declare const __reproTestHooks: {
|
|
|
485
460
|
shareRuntimePrivacyStateForTest: typeof shareRuntimePrivacyState;
|
|
486
461
|
setFullValueCaptureEnabledForTest(enabled: boolean): void;
|
|
487
462
|
scheduleSdkBackgroundWorkForTest(work: () => Promise<void> | void, options?: {
|
|
488
|
-
priority?:
|
|
463
|
+
priority?: "high" | "normal";
|
|
489
464
|
}): void;
|
|
490
465
|
drainSdkBackgroundQueueForTest(): Promise<void>;
|
|
491
466
|
resetSdkBackgroundQueuesForTest(): void;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.__reproTestHooks =
|
|
3
|
+
exports.__reproTestHooks = void 0;
|
|
4
|
+
exports.setDisabledFunctionTraces = setDisabledFunctionTraces;
|
|
5
|
+
exports.setDisabledFunctionTypes = setDisabledFunctionTypes;
|
|
6
|
+
exports.setDisabledTraceFiles = setDisabledTraceFiles;
|
|
7
|
+
exports.setReproTraceLogsEnabled = setReproTraceLogsEnabled;
|
|
8
|
+
exports.enableReproTraceLogs = enableReproTraceLogs;
|
|
9
|
+
exports.disableReproTraceLogs = disableReproTraceLogs;
|
|
10
|
+
exports.initReproTracing = initReproTracing;
|
|
11
|
+
exports.isReproTracingEnabled = isReproTracingEnabled;
|
|
12
|
+
exports.__materializePendingTraceEventsForWorker = __materializePendingTraceEventsForWorker;
|
|
13
|
+
exports.reproMiddleware = reproMiddleware;
|
|
14
|
+
exports.reproMongoosePlugin = reproMongoosePlugin;
|
|
15
|
+
exports.patchKafkaJs = patchKafkaJs;
|
|
16
|
+
exports.patchAmqplib = patchAmqplib;
|
|
17
|
+
exports.patchRedis = patchRedis;
|
|
18
|
+
exports.patchPostgreSql = patchPostgreSql;
|
|
19
|
+
exports.patchMongoDb = patchMongoDb;
|
|
20
|
+
exports.patchSendgridMail = patchSendgridMail;
|
|
21
|
+
exports.initRepro = initRepro;
|
|
4
22
|
const mongoose = require("mongoose");
|
|
5
23
|
const async_hooks_1 = require("async_hooks");
|
|
6
24
|
const crypto_1 = require("crypto");
|
|
@@ -514,11 +532,9 @@ function setDisabledFunctionTraces(rules) {
|
|
|
514
532
|
}
|
|
515
533
|
refreshDisabledFunctionTraceRules();
|
|
516
534
|
}
|
|
517
|
-
exports.setDisabledFunctionTraces = setDisabledFunctionTraces;
|
|
518
535
|
function setDisabledFunctionTypes(patterns) {
|
|
519
536
|
disabledFunctionTypePatterns = normalizePatternArray(patterns);
|
|
520
537
|
}
|
|
521
|
-
exports.setDisabledFunctionTypes = setDisabledFunctionTypes;
|
|
522
538
|
function flattenTraceFilePatterns(config) {
|
|
523
539
|
if (config === null || config === undefined)
|
|
524
540
|
return [];
|
|
@@ -541,7 +557,6 @@ function setDisabledTraceFiles(config) {
|
|
|
541
557
|
const items = Array.isArray(config) ? config : [config];
|
|
542
558
|
disabledTraceFilePatterns = items.flatMap(entry => flattenTraceFilePatterns(entry)).filter(Boolean);
|
|
543
559
|
}
|
|
544
|
-
exports.setDisabledTraceFiles = setDisabledTraceFiles;
|
|
545
560
|
function applyTraceLogPreference(tracer) {
|
|
546
561
|
if (__TRACE_LOG_PREF === null)
|
|
547
562
|
return;
|
|
@@ -554,11 +569,8 @@ function setReproTraceLogsEnabled(enabled) {
|
|
|
554
569
|
__TRACE_LOG_PREF = !!enabled;
|
|
555
570
|
applyTraceLogPreference(__TRACER__);
|
|
556
571
|
}
|
|
557
|
-
exports.setReproTraceLogsEnabled = setReproTraceLogsEnabled;
|
|
558
572
|
function enableReproTraceLogs() { setReproTraceLogsEnabled(true); }
|
|
559
|
-
exports.enableReproTraceLogs = enableReproTraceLogs;
|
|
560
573
|
function disableReproTraceLogs() { setReproTraceLogsEnabled(false); }
|
|
561
|
-
exports.disableReproTraceLogs = disableReproTraceLogs;
|
|
562
574
|
function summarizeEndpointFromEvents(events) {
|
|
563
575
|
let endpointTrace = null;
|
|
564
576
|
let preferredAppTrace = null;
|
|
@@ -665,10 +677,8 @@ function initReproTracing(opts) {
|
|
|
665
677
|
}
|
|
666
678
|
return __TRACER__;
|
|
667
679
|
}
|
|
668
|
-
exports.initReproTracing = initReproTracing;
|
|
669
680
|
/** Optional helper if users want to check it. */
|
|
670
681
|
function isReproTracingEnabled() { return __TRACER_READY; }
|
|
671
|
-
exports.isReproTracingEnabled = isReproTracingEnabled;
|
|
672
682
|
function captureSpanContextFromTracer(source) {
|
|
673
683
|
try {
|
|
674
684
|
// Prefer a preserved store captured at the call-site for thenables (e.g., Mongoose Query),
|
|
@@ -3127,7 +3137,6 @@ async function __materializePendingTraceEventsForWorker(payload) {
|
|
|
3127
3137
|
__FULL_VALUE_CAPTURE_ENABLED = prevFullValueCapture;
|
|
3128
3138
|
}
|
|
3129
3139
|
}
|
|
3130
|
-
exports.__materializePendingTraceEventsForWorker = __materializePendingTraceEventsForWorker;
|
|
3131
3140
|
function sanitizeRequestSnapshot(value) {
|
|
3132
3141
|
if (value === undefined)
|
|
3133
3142
|
return undefined;
|
|
@@ -5527,7 +5536,6 @@ function reproMiddleware(cfg) {
|
|
|
5527
5536
|
}));
|
|
5528
5537
|
};
|
|
5529
5538
|
}
|
|
5530
|
-
exports.reproMiddleware = reproMiddleware;
|
|
5531
5539
|
// ===================================================================
|
|
5532
5540
|
// reproMongoosePlugin — stable + NON-intrusive query logs
|
|
5533
5541
|
// - NO prototype monkey-patching of Mongoose
|
|
@@ -5937,7 +5945,6 @@ function reproMongoosePlugin(cfg) {
|
|
|
5937
5945
|
});
|
|
5938
5946
|
};
|
|
5939
5947
|
}
|
|
5940
|
-
exports.reproMongoosePlugin = reproMongoosePlugin;
|
|
5941
5948
|
function summarizeQueryResult(op, res) {
|
|
5942
5949
|
const resultPreview = sanitizeResultForMeta(res, { preserveLongStrings: true });
|
|
5943
5950
|
if (op === 'find' ||
|
|
@@ -7504,7 +7511,6 @@ function patchKafkaJs(cfg) {
|
|
|
7504
7511
|
}
|
|
7505
7512
|
}
|
|
7506
7513
|
}
|
|
7507
|
-
exports.patchKafkaJs = patchKafkaJs;
|
|
7508
7514
|
const AMQP_INTERNAL_PUBLISH_DEPTH = Symbol.for('repro.amqplib.internalPublishDepth');
|
|
7509
7515
|
function decodeAmqpHeaderValue(value) {
|
|
7510
7516
|
if (value === null || value === undefined)
|
|
@@ -7870,7 +7876,7 @@ function scheduleAmqpPublishCapture(params) {
|
|
|
7870
7876
|
url: params.path,
|
|
7871
7877
|
path: params.path,
|
|
7872
7878
|
status: params.status,
|
|
7873
|
-
durMs:
|
|
7879
|
+
durMs: params.durationMs,
|
|
7874
7880
|
key: params.key,
|
|
7875
7881
|
headers: {
|
|
7876
7882
|
transport: 'amqp',
|
|
@@ -7890,7 +7896,7 @@ function scheduleAmqpPublishCapture(params) {
|
|
|
7890
7896
|
entries: [{
|
|
7891
7897
|
actionId: params.aid ?? null,
|
|
7892
7898
|
request: requestPayload,
|
|
7893
|
-
t:
|
|
7899
|
+
t: params.completedAt,
|
|
7894
7900
|
}],
|
|
7895
7901
|
});
|
|
7896
7902
|
post(params.cfg, params.sid, {
|
|
@@ -7952,6 +7958,9 @@ function patchAmqplibPublishMethod(channel, cfg, methodName) {
|
|
|
7952
7958
|
if (finished)
|
|
7953
7959
|
return;
|
|
7954
7960
|
finished = true;
|
|
7961
|
+
// Capture queue and privacy processing time are not broker latency.
|
|
7962
|
+
const durationMs = Math.max(0, Date.now() - t0);
|
|
7963
|
+
const completedAt = alignedNow();
|
|
7955
7964
|
scheduleAmqpPublishCapture({
|
|
7956
7965
|
cfg,
|
|
7957
7966
|
sid,
|
|
@@ -7967,7 +7976,8 @@ function patchAmqplibPublishMethod(channel, cfg, methodName) {
|
|
|
7967
7976
|
content,
|
|
7968
7977
|
options: nextOptions,
|
|
7969
7978
|
span,
|
|
7970
|
-
|
|
7979
|
+
durationMs,
|
|
7980
|
+
completedAt,
|
|
7971
7981
|
status,
|
|
7972
7982
|
error,
|
|
7973
7983
|
result,
|
|
@@ -8100,6 +8110,8 @@ function wrapAmqplibConsumeHandler(onMessage, cfg, queue) {
|
|
|
8100
8110
|
finished = true;
|
|
8101
8111
|
if (!sid)
|
|
8102
8112
|
return;
|
|
8113
|
+
const durationMs = Math.max(0, Date.now() - t0);
|
|
8114
|
+
const completedAt = alignedNow();
|
|
8103
8115
|
const fields = message?.fields ?? {};
|
|
8104
8116
|
const messagePayload = buildAmqpMessagePayload(message?.content, properties);
|
|
8105
8117
|
const body = {
|
|
@@ -8129,7 +8141,7 @@ function wrapAmqplibConsumeHandler(onMessage, cfg, queue) {
|
|
|
8129
8141
|
url: `amqp://queue/${queueName}`,
|
|
8130
8142
|
path: `amqp://queue/${queueName}`,
|
|
8131
8143
|
status: threw ? 500 : 200,
|
|
8132
|
-
durMs:
|
|
8144
|
+
durMs: durationMs,
|
|
8133
8145
|
key: `AMQP_CONSUME ${queueName}`,
|
|
8134
8146
|
headers: normalizeAmqpHeadersForPayload(headers),
|
|
8135
8147
|
body: bodyMaterialization.value,
|
|
@@ -8140,7 +8152,7 @@ function wrapAmqplibConsumeHandler(onMessage, cfg, queue) {
|
|
|
8140
8152
|
entries: [{
|
|
8141
8153
|
actionId: aid ?? null,
|
|
8142
8154
|
request: requestPayload,
|
|
8143
|
-
t:
|
|
8155
|
+
t: completedAt,
|
|
8144
8156
|
}],
|
|
8145
8157
|
});
|
|
8146
8158
|
scheduleAmqpTraceFlush(cfg, amqpCtx, aid, consumeRid, rawTraceEvents, maskReq, captureScope);
|
|
@@ -8304,7 +8316,6 @@ function patchAmqplib(cfg) {
|
|
|
8304
8316
|
patchAmqplibModule(amqpModule, cfg);
|
|
8305
8317
|
}
|
|
8306
8318
|
}
|
|
8307
|
-
exports.patchAmqplib = patchAmqplib;
|
|
8308
8319
|
const redisPatchConfigs = [];
|
|
8309
8320
|
let redisRequireHookInstalled = false;
|
|
8310
8321
|
const REDIS_KEYLESS_COMMANDS = new Set([
|
|
@@ -8594,7 +8605,6 @@ function patchRedis(cfg) {
|
|
|
8594
8605
|
patchRedisFromModuleExports(redisModule, cfg);
|
|
8595
8606
|
}
|
|
8596
8607
|
}
|
|
8597
|
-
exports.patchRedis = patchRedis;
|
|
8598
8608
|
const postgresSqlPatchConfigs = [];
|
|
8599
8609
|
const postgresSqlSourceAls = new async_hooks_1.AsyncLocalStorage();
|
|
8600
8610
|
const pgPoolQueryAls = new async_hooks_1.AsyncLocalStorage();
|
|
@@ -9152,7 +9162,6 @@ function patchPostgreSql(cfg) {
|
|
|
9152
9162
|
for (const loaded of typeOrmModules)
|
|
9153
9163
|
patchTypeOrmFromModuleExports(loaded);
|
|
9154
9164
|
}
|
|
9155
|
-
exports.patchPostgreSql = patchPostgreSql;
|
|
9156
9165
|
const mongoDbPatchConfigs = [];
|
|
9157
9166
|
let mongoDbRequireHookInstalled = false;
|
|
9158
9167
|
let mongoDbImportHookInstalled = false;
|
|
@@ -9735,7 +9744,6 @@ function patchMongoDb(cfg) {
|
|
|
9735
9744
|
patchMongoDbFromModuleExports(mongoModule, cfg);
|
|
9736
9745
|
}
|
|
9737
9746
|
}
|
|
9738
|
-
exports.patchMongoDb = patchMongoDb;
|
|
9739
9747
|
// ===================================================================
|
|
9740
9748
|
// Auto init helpers
|
|
9741
9749
|
// ===================================================================
|
|
@@ -10203,7 +10211,6 @@ function patchSendgridMail(cfg) {
|
|
|
10203
10211
|
}
|
|
10204
10212
|
}
|
|
10205
10213
|
}
|
|
10206
|
-
exports.patchSendgridMail = patchSendgridMail;
|
|
10207
10214
|
/**
|
|
10208
10215
|
* Datadog-style one-time init.
|
|
10209
10216
|
* Enables tracing, auto-patches integrations, auto-registers mongoose plugin,
|
|
@@ -10251,7 +10258,6 @@ async function initRepro(cfg) {
|
|
|
10251
10258
|
enqueueRuntimeServiceRegistration(reproAutoMiddlewareConfig);
|
|
10252
10259
|
await initializeRuntimePrivacyPolicyAtStartup(reproAutoMiddlewareConfig);
|
|
10253
10260
|
}
|
|
10254
|
-
exports.initRepro = initRepro;
|
|
10255
10261
|
/** @internal Test hooks for runtime privacy policy wiring. */
|
|
10256
10262
|
exports.__reproTestHooks = {
|
|
10257
10263
|
shareRuntimePrivacyStateForTest: shareRuntimePrivacyState,
|
package/dist/ingest/client.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ export type EnqueueIngestOptions = {
|
|
|
3
3
|
priority?: 'normal' | 'high' | 'critical';
|
|
4
4
|
};
|
|
5
5
|
export declare const configureIngestQueueDrain: (options: {
|
|
6
|
-
shouldDefer?: ((enqueuedAt?: number) => boolean) | null
|
|
7
|
-
deferMs?: number
|
|
6
|
+
shouldDefer?: ((enqueuedAt?: number) => boolean) | null;
|
|
7
|
+
deferMs?: number;
|
|
8
8
|
}) => void;
|
|
9
9
|
export declare const kickIngestQueueDrain: () => void;
|
|
10
10
|
export declare const postIngestEntries: (config: IngestClientConfig, sessionId: string, entries: LegacyEntry[]) => Promise<void>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.getCtx = void 0;
|
|
4
|
+
exports.patchSendgridMail = patchSendgridMail;
|
|
4
5
|
// repro-node/src/integrations/sendgrid.ts
|
|
5
6
|
const async_hooks_1 = require("async_hooks");
|
|
6
7
|
const client_1 = require("../ingest/client");
|
|
@@ -164,4 +165,3 @@ function patchSendgridMail(cfg) {
|
|
|
164
165
|
return undefined;
|
|
165
166
|
}
|
|
166
167
|
}
|
|
167
|
-
exports.patchSendgridMail = patchSendgridMail;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.LIVE_CAPTURE_LIMITS = void 0;
|
|
4
|
+
exports.limitLiveCapture = limitLiveCapture;
|
|
5
|
+
exports.collectCaptureOmissions = collectCaptureOmissions;
|
|
6
|
+
exports.limitLiveArguments = limitLiveArguments;
|
|
4
7
|
const http_1 = require("http");
|
|
5
8
|
const semantic_serialization_1 = require("./semantic-serialization");
|
|
6
9
|
exports.LIVE_CAPTURE_LIMITS = Object.freeze({ maxDepth: 10, maxKeys: 100, maxEntries: 100 });
|
|
@@ -170,7 +173,6 @@ function limitLiveCapture(value, rootPath = '$') {
|
|
|
170
173
|
return { value: omit({ reason: 'uninspectable', path: rootPath, capturedCount: 0 }), omissions };
|
|
171
174
|
}
|
|
172
175
|
}
|
|
173
|
-
exports.limitLiveCapture = limitLiveCapture;
|
|
174
176
|
function collectCaptureOmissions(value) {
|
|
175
177
|
const result = [];
|
|
176
178
|
const seen = new WeakSet();
|
|
@@ -193,7 +195,6 @@ function collectCaptureOmissions(value) {
|
|
|
193
195
|
}
|
|
194
196
|
return result;
|
|
195
197
|
}
|
|
196
|
-
exports.collectCaptureOmissions = collectCaptureOmissions;
|
|
197
198
|
function limitLiveArguments(args) {
|
|
198
199
|
const values = args.slice(0, 100).map((arg, index) => limitLiveCapture(arg, `$[${index}]`).value);
|
|
199
200
|
if (args.length > 100) {
|
|
@@ -204,4 +205,3 @@ function limitLiveArguments(args) {
|
|
|
204
205
|
}
|
|
205
206
|
return values;
|
|
206
207
|
}
|
|
207
|
-
exports.limitLiveArguments = limitLiveArguments;
|
package/dist/privacy-fallback.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.sanitizeUnknownBoundaryTextWithDetectors =
|
|
3
|
+
exports.sanitizeUnknownBoundaryTextWithDetectors = sanitizeUnknownBoundaryTextWithDetectors;
|
|
4
4
|
const SANITIZED_TEXT_CACHE = new Map();
|
|
5
5
|
const SANITIZED_TEXT_CACHE_MAX = 500;
|
|
6
6
|
async function sanitizeUnknownBoundaryTextWithDetectors(value) {
|
|
@@ -14,7 +14,6 @@ async function sanitizeUnknownBoundaryTextWithDetectors(value) {
|
|
|
14
14
|
rememberSanitizedText(value, value);
|
|
15
15
|
return value;
|
|
16
16
|
}
|
|
17
|
-
exports.sanitizeUnknownBoundaryTextWithDetectors = sanitizeUnknownBoundaryTextWithDetectors;
|
|
18
17
|
function rememberSanitizedText(input, output) {
|
|
19
18
|
SANITIZED_TEXT_CACHE.set(input, output);
|
|
20
19
|
if (SANITIZED_TEXT_CACHE.size <= SANITIZED_TEXT_CACHE_MAX) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.redactEmailDeterministic = redactEmailDeterministic;
|
|
4
|
+
exports.redactEmailsInText = redactEmailsInText;
|
|
5
|
+
exports.collectEmailsInText = collectEmailsInText;
|
|
4
6
|
const crypto_1 = require("crypto");
|
|
5
7
|
const EMAIL_PATTERN = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
|
|
6
8
|
const EMAIL_TOKEN_PREFIX = 'email_tok_';
|
|
@@ -13,14 +15,12 @@ function redactEmailDeterministic(value) {
|
|
|
13
15
|
const token = (0, crypto_1.createHash)('sha256').update(normalized).digest('hex').slice(0, EMAIL_TOKEN_LENGTH);
|
|
14
16
|
return `${EMAIL_TOKEN_PREFIX}${token}`;
|
|
15
17
|
}
|
|
16
|
-
exports.redactEmailDeterministic = redactEmailDeterministic;
|
|
17
18
|
function redactEmailsInText(value) {
|
|
18
19
|
if (!value) {
|
|
19
20
|
return value;
|
|
20
21
|
}
|
|
21
22
|
return value.replace(EMAIL_PATTERN, (match) => isAlreadyMaskedEmail(match) ? match : redactEmailDeterministic(match));
|
|
22
23
|
}
|
|
23
|
-
exports.redactEmailsInText = redactEmailsInText;
|
|
24
24
|
function collectEmailsInText(value) {
|
|
25
25
|
if (!value) {
|
|
26
26
|
return [];
|
|
@@ -28,7 +28,6 @@ function collectEmailsInText(value) {
|
|
|
28
28
|
const matches = value.match(EMAIL_PATTERN);
|
|
29
29
|
return Array.isArray(matches) ? matches : [];
|
|
30
30
|
}
|
|
31
|
-
exports.collectEmailsInText = collectEmailsInText;
|
|
32
31
|
function isAlreadyMaskedEmail(value) {
|
|
33
32
|
if (/^email_tok_[a-z0-9]+$/i.test(value)) {
|
|
34
33
|
return true;
|
package/dist/privacy.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.clearRuntimePrivacyProvenance = clearRuntimePrivacyProvenance;
|
|
4
|
+
exports.normalizePrivacyEnvironment = normalizePrivacyEnvironment;
|
|
5
|
+
exports.resolvePrivacyApiBase = resolvePrivacyApiBase;
|
|
6
|
+
exports.fetchApprovedRuntimePrivacyPolicy = fetchApprovedRuntimePrivacyPolicy;
|
|
7
|
+
exports.normalizeRuntimePrivacyPolicy = normalizeRuntimePrivacyPolicy;
|
|
8
|
+
exports.applyRuntimePrivacyPolicy = applyRuntimePrivacyPolicy;
|
|
9
|
+
exports.applyRuntimePrivacyPolicyAsync = applyRuntimePrivacyPolicyAsync;
|
|
4
10
|
const crypto_1 = require("crypto");
|
|
5
11
|
const capture_work_budget_1 = require("./capture-work-budget");
|
|
6
12
|
const privacy_fallback_1 = require("./privacy-fallback");
|
|
@@ -21,7 +27,6 @@ function clearRuntimePrivacyProvenance() {
|
|
|
21
27
|
objectRuntimePrivacyProvenance = new WeakMap();
|
|
22
28
|
serializedRuntimePrivacyProvenance.clear();
|
|
23
29
|
}
|
|
24
|
-
exports.clearRuntimePrivacyProvenance = clearRuntimePrivacyProvenance;
|
|
25
30
|
const ALL_SURFACES = [
|
|
26
31
|
'db.pk',
|
|
27
32
|
'db.before',
|
|
@@ -79,7 +84,6 @@ function normalizePrivacyEnvironment(input) {
|
|
|
79
84
|
return 'qa';
|
|
80
85
|
return normalized || 'dev';
|
|
81
86
|
}
|
|
82
|
-
exports.normalizePrivacyEnvironment = normalizePrivacyEnvironment;
|
|
83
87
|
function resolvePrivacyApiBase(cfg) {
|
|
84
88
|
const value = cfg?.apiBase ||
|
|
85
89
|
process.env.REPRO_PRIVACY_API_BASE ||
|
|
@@ -90,7 +94,6 @@ function resolvePrivacyApiBase(cfg) {
|
|
|
90
94
|
}
|
|
91
95
|
return String(value).replace(/\/+$/, '');
|
|
92
96
|
}
|
|
93
|
-
exports.resolvePrivacyApiBase = resolvePrivacyApiBase;
|
|
94
97
|
const DEFAULT_RUNTIME_PRIVACY_FETCH_TIMEOUT_MS = 10000;
|
|
95
98
|
function resolveRuntimePrivacyFetchTimeoutMs(value) {
|
|
96
99
|
if (!Number.isFinite(value)) {
|
|
@@ -162,7 +165,6 @@ async function fetchApprovedRuntimePrivacyPolicy(params) {
|
|
|
162
165
|
}
|
|
163
166
|
}
|
|
164
167
|
}
|
|
165
|
-
exports.fetchApprovedRuntimePrivacyPolicy = fetchApprovedRuntimePrivacyPolicy;
|
|
166
168
|
function normalizeRuntimePrivacyPolicy(raw) {
|
|
167
169
|
if (!raw || typeof raw !== 'object') {
|
|
168
170
|
return null;
|
|
@@ -189,21 +191,18 @@ function normalizeRuntimePrivacyPolicy(raw) {
|
|
|
189
191
|
runtimePrivacyStatementIndexes.set(normalizedPolicy, buildRuntimePrivacyStatementIndex(statements));
|
|
190
192
|
return normalizedPolicy;
|
|
191
193
|
}
|
|
192
|
-
exports.normalizeRuntimePrivacyPolicy = normalizeRuntimePrivacyPolicy;
|
|
193
194
|
function applyRuntimePrivacyPolicy(policy, surface, value, context) {
|
|
194
195
|
if (value === undefined) {
|
|
195
196
|
return value;
|
|
196
197
|
}
|
|
197
198
|
return transformValue(policy ?? EMPTY_RUNTIME_PRIVACY_POLICY, surface, value, context, []);
|
|
198
199
|
}
|
|
199
|
-
exports.applyRuntimePrivacyPolicy = applyRuntimePrivacyPolicy;
|
|
200
200
|
async function applyRuntimePrivacyPolicyAsync(policy, surface, value, context) {
|
|
201
201
|
if (value === undefined) {
|
|
202
202
|
return value;
|
|
203
203
|
}
|
|
204
204
|
return transformValueAsync(policy ?? EMPTY_RUNTIME_PRIVACY_POLICY, surface, value, context, []);
|
|
205
205
|
}
|
|
206
|
-
exports.applyRuntimePrivacyPolicyAsync = applyRuntimePrivacyPolicyAsync;
|
|
207
206
|
function flattenStatements(policy) {
|
|
208
207
|
const fromGroups = Array.isArray(policy.groups)
|
|
209
208
|
? policy.groups.flatMap((group) => Array.isArray(group?.statements) ? group.statements : [])
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.mongooseDocumentData = mongooseDocumentData;
|
|
4
|
+
exports.projectSemanticInfrastructureValue = projectSemanticInfrastructureValue;
|
|
4
5
|
function constructorName(value) {
|
|
5
6
|
if (!value || typeof value !== 'object')
|
|
6
7
|
return '';
|
|
@@ -55,7 +56,6 @@ function mongooseDocumentData(value) {
|
|
|
55
56
|
return null;
|
|
56
57
|
return method(value, '$__init') && method(value, '$toObject') ? doc : null;
|
|
57
58
|
}
|
|
58
|
-
exports.mongooseDocumentData = mongooseDocumentData;
|
|
59
59
|
function primitiveStateValue(value) {
|
|
60
60
|
if (value === null)
|
|
61
61
|
return null;
|
|
@@ -200,4 +200,3 @@ function projectSemanticInfrastructureValue(value) {
|
|
|
200
200
|
projectPostgres(value, className) ||
|
|
201
201
|
projectTypeOrm(value, className));
|
|
202
202
|
}
|
|
203
|
-
exports.projectSemanticInfrastructureValue = projectSemanticInfrastructureValue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reproapp/node-sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.56",
|
|
4
4
|
"description": "Repro Nest SDK",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"test:latency": "npm run build && node test/privacy-fetch-timeout.test.js && node test/capture-latency.testcontainers.test.js",
|
|
14
14
|
"test:live-capture": "npm run build && node test/twelve-element-capture.testcontainers.test.js && node test/live-capture-limits.test.js && node test/ingest-throttle-retention.test.js && node test/live-capture-limits.testcontainers.test.js",
|
|
15
15
|
"test:live-brokers": "npm run build && node test/live-broker-limits.testcontainers.test.js",
|
|
16
|
+
"test:amqp-timing": "npm run build && node test/amqplib-integration.test.js && node test/amqplib-timing.testcontainers.test.js",
|
|
16
17
|
"build": "tsc -p tsconfig.json",
|
|
17
18
|
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
|
18
19
|
"prepublishOnly": "npm run build",
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
"mongoose": ">=6"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
28
|
+
"typescript": "^5.7.3",
|
|
27
29
|
"@nestjs/typeorm": "^11.0.0",
|
|
28
30
|
"@testcontainers/kafka": "^11.8.1",
|
|
29
31
|
"@types/express": "^5.0.3",
|
package/src/index.ts
CHANGED
|
@@ -9498,7 +9498,8 @@ function scheduleAmqpPublishCapture(params: {
|
|
|
9498
9498
|
content: any;
|
|
9499
9499
|
options: any;
|
|
9500
9500
|
span: SpanContext | null;
|
|
9501
|
-
|
|
9501
|
+
durationMs: number;
|
|
9502
|
+
completedAt: number;
|
|
9502
9503
|
status: number;
|
|
9503
9504
|
error?: any;
|
|
9504
9505
|
result?: any;
|
|
@@ -9540,7 +9541,7 @@ function scheduleAmqpPublishCapture(params: {
|
|
|
9540
9541
|
url: params.path,
|
|
9541
9542
|
path: params.path,
|
|
9542
9543
|
status: params.status,
|
|
9543
|
-
durMs:
|
|
9544
|
+
durMs: params.durationMs,
|
|
9544
9545
|
key: params.key,
|
|
9545
9546
|
headers: {
|
|
9546
9547
|
transport: 'amqp',
|
|
@@ -9561,7 +9562,7 @@ function scheduleAmqpPublishCapture(params: {
|
|
|
9561
9562
|
entries: [{
|
|
9562
9563
|
actionId: params.aid ?? null,
|
|
9563
9564
|
request: requestPayload,
|
|
9564
|
-
t:
|
|
9565
|
+
t: params.completedAt,
|
|
9565
9566
|
}],
|
|
9566
9567
|
});
|
|
9567
9568
|
post(params.cfg, params.sid!, {
|
|
@@ -9634,6 +9635,9 @@ function patchAmqplibPublishMethod(
|
|
|
9634
9635
|
const finish = (status: number, error?: any, result?: any) => {
|
|
9635
9636
|
if (finished) return;
|
|
9636
9637
|
finished = true;
|
|
9638
|
+
// Capture queue and privacy processing time are not broker latency.
|
|
9639
|
+
const durationMs = Math.max(0, Date.now() - t0);
|
|
9640
|
+
const completedAt = alignedNow();
|
|
9637
9641
|
scheduleAmqpPublishCapture({
|
|
9638
9642
|
cfg,
|
|
9639
9643
|
sid,
|
|
@@ -9649,7 +9653,8 @@ function patchAmqplibPublishMethod(
|
|
|
9649
9653
|
content,
|
|
9650
9654
|
options: nextOptions,
|
|
9651
9655
|
span,
|
|
9652
|
-
|
|
9656
|
+
durationMs,
|
|
9657
|
+
completedAt,
|
|
9653
9658
|
status,
|
|
9654
9659
|
error,
|
|
9655
9660
|
result,
|
|
@@ -9779,6 +9784,8 @@ function wrapAmqplibConsumeHandler(onMessage: any, cfg: AmqplibPatchConfig, queu
|
|
|
9779
9784
|
if (finished) return;
|
|
9780
9785
|
finished = true;
|
|
9781
9786
|
if (!sid) return;
|
|
9787
|
+
const durationMs = Math.max(0, Date.now() - t0);
|
|
9788
|
+
const completedAt = alignedNow();
|
|
9782
9789
|
const fields = message?.fields ?? {};
|
|
9783
9790
|
const messagePayload = buildAmqpMessagePayload(message?.content, properties);
|
|
9784
9791
|
const body = {
|
|
@@ -9814,7 +9821,7 @@ function wrapAmqplibConsumeHandler(onMessage: any, cfg: AmqplibPatchConfig, queu
|
|
|
9814
9821
|
url: `amqp://queue/${queueName}`,
|
|
9815
9822
|
path: `amqp://queue/${queueName}`,
|
|
9816
9823
|
status: threw ? 500 : 200,
|
|
9817
|
-
durMs:
|
|
9824
|
+
durMs: durationMs,
|
|
9818
9825
|
key: `AMQP_CONSUME ${queueName}`,
|
|
9819
9826
|
headers: normalizeAmqpHeadersForPayload(headers),
|
|
9820
9827
|
body: bodyMaterialization.value,
|
|
@@ -9826,7 +9833,7 @@ function wrapAmqplibConsumeHandler(onMessage: any, cfg: AmqplibPatchConfig, queu
|
|
|
9826
9833
|
entries: [{
|
|
9827
9834
|
actionId: aid ?? null,
|
|
9828
9835
|
request: requestPayload,
|
|
9829
|
-
t:
|
|
9836
|
+
t: completedAt,
|
|
9830
9837
|
}],
|
|
9831
9838
|
});
|
|
9832
9839
|
scheduleAmqpTraceFlush(cfg, amqpCtx, aid, consumeRid, rawTraceEvents, maskReq, captureScope);
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
process.env.REPRO_SDK_BACKGROUND_MAX_DEFER_MS = '0';
|
|
2
|
+
process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '0';
|
|
3
|
+
process.env.REPRO_SDK_BACKGROUND_BUSY_IDLE_DELAY_MS = '0';
|
|
4
|
+
process.env.TRACE_IDLE_FLUSH_MS = '0';
|
|
5
|
+
process.env.TRACE_LINGER_AFTER_FINISH_MS = '0';
|
|
6
|
+
|
|
7
|
+
const assert = require('node:assert/strict');
|
|
8
|
+
const http = require('node:http');
|
|
9
|
+
const { once } = require('node:events');
|
|
10
|
+
const { GenericContainer, Wait } = require('testcontainers');
|
|
11
|
+
const amqp = require('amqplib');
|
|
12
|
+
const { __reproTestHooks: hooks } = require('../dist');
|
|
13
|
+
const { flushIngestQueue } = require('../dist/ingest/client');
|
|
14
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
15
|
+
const deferred = () => {
|
|
16
|
+
let resolve;
|
|
17
|
+
const promise = new Promise(done => { resolve = done; });
|
|
18
|
+
return { promise, resolve };
|
|
19
|
+
};
|
|
20
|
+
async function within(promise, label) {
|
|
21
|
+
let timer;
|
|
22
|
+
try {
|
|
23
|
+
return await Promise.race([promise, new Promise((_, reject) => {
|
|
24
|
+
timer = setTimeout(() => reject(new Error(`Timed out: ${label}`)), 10000);
|
|
25
|
+
})]);
|
|
26
|
+
} finally { clearTimeout(timer); }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function main() {
|
|
30
|
+
const events = [], channels = [];
|
|
31
|
+
let rabbit, connection, release;
|
|
32
|
+
const sink = http.createServer(async (req, res) => {
|
|
33
|
+
const buffers = [];
|
|
34
|
+
for await (const chunk of req) buffers.push(chunk);
|
|
35
|
+
events.push(...(JSON.parse(Buffer.concat(buffers)).events || []));
|
|
36
|
+
res.writeHead(202); res.end('{}');
|
|
37
|
+
});
|
|
38
|
+
sink.listen(0, '127.0.0.1'); await once(sink, 'listening');
|
|
39
|
+
try {
|
|
40
|
+
rabbit = await new GenericContainer('rabbitmq:3.13-alpine')
|
|
41
|
+
.withEnvironment({ RABBITMQ_DEFAULT_USER: 'repro', RABBITMQ_DEFAULT_PASS: 'test' })
|
|
42
|
+
.withExposedPorts(5672).withWaitStrategy(Wait.forLogMessage('Server startup complete')).start();
|
|
43
|
+
connection = await amqp.connect(`amqp://repro:test@${rabbit.getHost()}:${rabbit.getMappedPort(5672)}`);
|
|
44
|
+
for (const mode of ['live', 'staging']) {
|
|
45
|
+
const sid = `S_amqp_timing_${mode}`;
|
|
46
|
+
const cfg = { tenantId: 'TENANT_timing', appId: 'APP_timing', appSecret: 'test',
|
|
47
|
+
privacy: { captureRaw: true, environment: 'dev' },
|
|
48
|
+
ingestBase: `http://127.0.0.1:${sink.address().port}`,
|
|
49
|
+
resolveContext: () => ({ sid, aid: `A_${mode}` }) };
|
|
50
|
+
const channel = await connection.createConfirmChannel(); channels.push(channel);
|
|
51
|
+
hooks.patchAmqplibChannelForTest(channel, cfg);
|
|
52
|
+
const queue = `timing-${mode}`;
|
|
53
|
+
await channel.assertQueue(queue, { durable: false });
|
|
54
|
+
const handled = deferred(), acknowledged = deferred(), gate = deferred(), started = deferred();
|
|
55
|
+
release = gate.resolve;
|
|
56
|
+
// Hold the real capture queue, not the broker or application callback.
|
|
57
|
+
hooks.scheduleSdkBackgroundWorkForTest(async () => {
|
|
58
|
+
started.resolve(); await gate.promise;
|
|
59
|
+
}, { priority: 'high' });
|
|
60
|
+
await within(started.promise, 'capture queue blocker');
|
|
61
|
+
let handlerStart, handlerEnd, publishEnd;
|
|
62
|
+
const input = { orderId: `order-${mode}`, lines: [{ sku: 'item', quantity: 3 }] };
|
|
63
|
+
const consumer = await channel.consume(queue, async message => {
|
|
64
|
+
handlerStart = Date.now();
|
|
65
|
+
assert.deepEqual(JSON.parse(message.content.toString()), input);
|
|
66
|
+
assert.equal(message.properties.headers['x-bug-session-id'], sid);
|
|
67
|
+
assert.equal(message.properties.headers['x-repro-capture-mode'], mode);
|
|
68
|
+
await sleep(60);
|
|
69
|
+
channel.ack(message);
|
|
70
|
+
handlerEnd = Date.now(); handled.resolve();
|
|
71
|
+
});
|
|
72
|
+
const publishStart = Date.now();
|
|
73
|
+
const options = { headers: { 'x-repro-capture-mode': mode }, persistent: false };
|
|
74
|
+
const result = hooks.runWithRequestContextForTest({ sid, aid: `A_${mode}`, captureScope: { liveCapture: mode === 'live' } },
|
|
75
|
+
() => channel.sendToQueue(queue, Buffer.from(JSON.stringify(input)), options, error => {
|
|
76
|
+
assert.ifError(error); publishEnd = Date.now(); acknowledged.resolve();
|
|
77
|
+
}));
|
|
78
|
+
assert.equal(typeof result, 'boolean', 'publish must preserve amqplib backpressure contract');
|
|
79
|
+
await within(Promise.all([handled.promise, acknowledged.promise]), 'broker confirm and consumer acknowledgement');
|
|
80
|
+
assert.equal(options.headers['x-bug-session-id'], undefined, 'caller options stay unchanged');
|
|
81
|
+
assert.equal(events.filter(e => e.session_id === sid && e.event_type === 'backend_request').length, 0);
|
|
82
|
+
await sleep(450);
|
|
83
|
+
const releasedAt = Date.now(); gate.resolve(); release = null;
|
|
84
|
+
const deadline = Date.now() + 10000;
|
|
85
|
+
let requests;
|
|
86
|
+
do {
|
|
87
|
+
await hooks.drainSdkBackgroundQueueForTest(); await flushIngestQueue();
|
|
88
|
+
requests = events.filter(e => e.session_id === sid && e.event_type === 'backend_request');
|
|
89
|
+
if (requests.length === 2) break;
|
|
90
|
+
await sleep(20);
|
|
91
|
+
} while (Date.now() < deadline);
|
|
92
|
+
assert.equal(requests.length, 2, 'exactly one publish and one consume, despite sendToQueue delegating to publish');
|
|
93
|
+
for (const [method, start, end] of [
|
|
94
|
+
['AMQP_SEND_TO_QUEUE', publishStart, publishEnd],
|
|
95
|
+
['AMQP_CONSUME', handlerStart, handlerEnd],
|
|
96
|
+
]) {
|
|
97
|
+
const event = requests.find(e => e.payload.request.method === method);
|
|
98
|
+
assert(event, `${mode}: missing ${method}`);
|
|
99
|
+
const request = event.payload.request;
|
|
100
|
+
assert(request.durMs <= end - start + 30,
|
|
101
|
+
`${mode} ${method}: ${request.durMs}ms includes capture queue time; business took ${end - start}ms`);
|
|
102
|
+
assert(request.durMs >= end - start - 10, 'duration must still include the async business handler');
|
|
103
|
+
const timestamp = Date.parse(event.event_ts);
|
|
104
|
+
assert(timestamp <= end + 30 && timestamp >= start,
|
|
105
|
+
`event time must be operation completion, not background delivery: ${timestamp}, ${end}`);
|
|
106
|
+
assert(timestamp < releasedAt - 400);
|
|
107
|
+
assert.deepEqual(request.body.value, input, 'timing correction must not lose payload');
|
|
108
|
+
}
|
|
109
|
+
await channel.cancel(consumer.consumerTag);
|
|
110
|
+
console.log(`${mode}: broker confirms and async handler finish before capture drain; timings and payload exact`);
|
|
111
|
+
}
|
|
112
|
+
// Exercise failure/return contracts with real delivered envelopes. The
|
|
113
|
+
// adapter catches deliberate handler failures just as an application would.
|
|
114
|
+
const sid = 'S_amqp_failures';
|
|
115
|
+
const cfg = { tenantId: 'TENANT_timing', appId: 'APP_timing', appSecret: 'test',
|
|
116
|
+
privacy: { captureRaw: true, environment: 'dev' },
|
|
117
|
+
ingestBase: `http://127.0.0.1:${sink.address().port}`,
|
|
118
|
+
resolveContext: () => ({ sid, aid: 'A_failures' }) };
|
|
119
|
+
const raw = await connection.createChannel(); channels.push(raw);
|
|
120
|
+
await raw.assertQueue('timing-contracts', { durable: false });
|
|
121
|
+
const gate = deferred(), started = deferred(); release = gate.resolve;
|
|
122
|
+
hooks.scheduleSdkBackgroundWorkForTest(async () => { started.resolve(); await gate.promise; }, { priority: 'high' });
|
|
123
|
+
await within(started.promise, 'failure capture queue blocker');
|
|
124
|
+
const observations = new Map(), completed = deferred();
|
|
125
|
+
const identity = { result: 'unchanged' }, failure = new Error('application failure');
|
|
126
|
+
const wrapped = hooks.wrapAmqplibConsumeHandlerForTest(message => {
|
|
127
|
+
const { scenario } = JSON.parse(message.content.toString());
|
|
128
|
+
const observation = { start: Date.now(), calls: 1, scenario };
|
|
129
|
+
observations.set(scenario, observation);
|
|
130
|
+
const finish = () => {
|
|
131
|
+
observation.end = Date.now();
|
|
132
|
+
if (scenario.endsWith('error')) throw failure;
|
|
133
|
+
return identity;
|
|
134
|
+
};
|
|
135
|
+
return scenario.startsWith('async') ? sleep(60).then(finish) : finish();
|
|
136
|
+
}, cfg, 'timing-contracts');
|
|
137
|
+
let handled = 0;
|
|
138
|
+
const consumer = await raw.consume('timing-contracts', message => {
|
|
139
|
+
const { scenario } = JSON.parse(message.content.toString());
|
|
140
|
+
const complete = () => { raw.ack(message); if (++handled === 4) completed.resolve(); };
|
|
141
|
+
try {
|
|
142
|
+
const value = wrapped(message);
|
|
143
|
+
if (scenario.startsWith('async')) {
|
|
144
|
+
assert.equal(typeof value.then, 'function');
|
|
145
|
+
value.then(result => {
|
|
146
|
+
assert.strictEqual(result, identity); complete();
|
|
147
|
+
}, error => { assert.strictEqual(error, failure); complete(); });
|
|
148
|
+
} else { assert.strictEqual(value, identity); complete(); }
|
|
149
|
+
} catch (error) { assert.strictEqual(error, failure); complete(); }
|
|
150
|
+
});
|
|
151
|
+
for (const scenario of ['sync-success', 'async-success', 'sync-error', 'async-error']) {
|
|
152
|
+
raw.sendToQueue('timing-contracts', Buffer.from(JSON.stringify({ scenario })), { headers: {
|
|
153
|
+
'x-bug-session-id': sid, 'x-bug-action-id': 'A_failures', 'x-repro-capture-mode': 'live',
|
|
154
|
+
} });
|
|
155
|
+
}
|
|
156
|
+
await within(completed.promise, 'handler return and error contracts');
|
|
157
|
+
const invalid = await connection.createConfirmChannel(); channels.push(invalid);
|
|
158
|
+
invalid.on('error', () => {});
|
|
159
|
+
hooks.patchAmqplibChannelForTest(invalid, cfg);
|
|
160
|
+
const rejected = deferred();
|
|
161
|
+
const rejectStart = Date.now(); let rejectEnd;
|
|
162
|
+
invalid.publish('missing-exchange-timing', 'route', Buffer.from('message'), {}, error => {
|
|
163
|
+
assert(error, 'broker must reject the nonexistent exchange');
|
|
164
|
+
rejectEnd = Date.now(); rejected.resolve();
|
|
165
|
+
});
|
|
166
|
+
await within(rejected.promise, 'broker rejection');
|
|
167
|
+
await sleep(450); gate.resolve(); release = null;
|
|
168
|
+
const deadline = Date.now() + 10000;
|
|
169
|
+
let requests;
|
|
170
|
+
do {
|
|
171
|
+
await hooks.drainSdkBackgroundQueueForTest(); await flushIngestQueue();
|
|
172
|
+
requests = events.filter(e => e.session_id === sid && e.event_type === 'backend_request');
|
|
173
|
+
if (requests.length === 5) break;
|
|
174
|
+
await sleep(20);
|
|
175
|
+
} while (Date.now() < deadline);
|
|
176
|
+
assert.equal(requests.length, 5, 'no retries/double captures on exceptions or broker channel close');
|
|
177
|
+
for (const event of requests) {
|
|
178
|
+
const request = event.payload.request;
|
|
179
|
+
const observation = request.method === 'AMQP_PUBLISH'
|
|
180
|
+
? { start: rejectStart, end: rejectEnd, scenario: 'broker-error' }
|
|
181
|
+
: observations.get(request.body.value.scenario);
|
|
182
|
+
assert(observation);
|
|
183
|
+
assert.equal(request.status, observation.scenario.endsWith('error') ? 500 : 200);
|
|
184
|
+
assert(request.durMs <= observation.end - observation.start + 30);
|
|
185
|
+
assert(Date.parse(event.event_ts) <= observation.end + 30);
|
|
186
|
+
}
|
|
187
|
+
await raw.cancel(consumer.consumerTag);
|
|
188
|
+
console.log('sync/async return and error identity preserved; broker rejection and failure timestamps exclude capture drain');
|
|
189
|
+
} finally {
|
|
190
|
+
release?.();
|
|
191
|
+
for (const channel of channels) await channel.close().catch(() => {});
|
|
192
|
+
if (connection) await connection.close();
|
|
193
|
+
await hooks.drainSdkBackgroundQueueForTest(); await flushIngestQueue();
|
|
194
|
+
sink.closeAllConnections(); await new Promise(resolve => sink.close(resolve));
|
|
195
|
+
if (rabbit) await rabbit.stop();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
main().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -25,7 +25,9 @@ async function main() {
|
|
|
25
25
|
const results = [];
|
|
26
26
|
try {
|
|
27
27
|
mongo = await new GenericContainer('mongo:7').withExposedPorts(27017).withWaitStrategy(Wait.forListeningPorts()).start();
|
|
28
|
-
|
|
28
|
+
const variants = ['off', 'live-raw', 'staging-raw', 'live-policy', 'staging-policy', 'live-policy-fallback', 'staging-policy-fallback', 'staging-policy-fault']
|
|
29
|
+
.filter(variant => !process.argv.includes('--raw-only') || !variant.includes('policy'));
|
|
30
|
+
for (const variant of variants) {
|
|
29
31
|
const mode = variant.split('-')[0];
|
|
30
32
|
child = fork(require.resolve('./fixtures/capture-latency-server'), [], {
|
|
31
33
|
env: { ...process.env, CAPTURE_TEST_MODE: mode,
|