@reproapp/node-sdk 0.0.13 → 0.0.15
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.js +105 -5
- package/dist/ingest/client.js +33 -0
- package/dist/ingest/mapper.js +4 -0
- package/dist/ingest/types.d.ts +2 -0
- package/package.json +2 -2
- package/src/index.ts +180 -45
- package/src/ingest/client.ts +32 -0
- package/src/ingest/mapper.ts +2 -0
- package/src/ingest/types.ts +2 -0
- package/test/ingest-timeout.test.js +67 -0
- package/test/request-flush-timing.test.js +17 -0
package/dist/index.js
CHANGED
|
@@ -783,6 +783,40 @@ function attachSpanContext(target, span) {
|
|
|
783
783
|
}
|
|
784
784
|
return target;
|
|
785
785
|
}
|
|
786
|
+
function createRequestCaptureScope(requestRid, transport) {
|
|
787
|
+
return {
|
|
788
|
+
requestRid,
|
|
789
|
+
transport,
|
|
790
|
+
backendRequestCount: 0,
|
|
791
|
+
traceBatchCount: 0,
|
|
792
|
+
dbChangeCount: 0,
|
|
793
|
+
emailEventCount: 0,
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
function buildRequestCaptureManifestPayload(scope, traceBatchTotal, closedAtMs) {
|
|
797
|
+
const expectedTraceBatches = Math.max(0, Math.trunc(traceBatchTotal));
|
|
798
|
+
const closedAt = typeof closedAtMs === 'number' && Number.isFinite(closedAtMs)
|
|
799
|
+
? closedAtMs
|
|
800
|
+
: alignedNow();
|
|
801
|
+
return {
|
|
802
|
+
scopeId: `req:${scope.requestRid}`,
|
|
803
|
+
requestRid: scope.requestRid,
|
|
804
|
+
transport: scope.transport,
|
|
805
|
+
expected: {
|
|
806
|
+
backendRequest: scope.backendRequestCount,
|
|
807
|
+
traceBatch: expectedTraceBatches,
|
|
808
|
+
dbChange: scope.dbChangeCount,
|
|
809
|
+
emailEvent: scope.emailEventCount,
|
|
810
|
+
},
|
|
811
|
+
traceBatch: expectedTraceBatches > 0
|
|
812
|
+
? {
|
|
813
|
+
from: 0,
|
|
814
|
+
to: expectedTraceBatches - 1,
|
|
815
|
+
}
|
|
816
|
+
: undefined,
|
|
817
|
+
closedAt,
|
|
818
|
+
};
|
|
819
|
+
}
|
|
786
820
|
const als = new async_hooks_1.AsyncLocalStorage();
|
|
787
821
|
const getCtx = () => als.getStore() || {};
|
|
788
822
|
function currentClockSkewMs() {
|
|
@@ -3879,6 +3913,7 @@ function reproMiddleware(cfg) {
|
|
|
3879
3913
|
const clockSkewMs = headerTs !== null ? headerTs - requestStartRaw : 0;
|
|
3880
3914
|
const requestEpochMs = headerTs !== null ? headerTs : requestStartRaw + clockSkewMs;
|
|
3881
3915
|
const rid = nextRequestId(requestEpochMs);
|
|
3916
|
+
const captureScope = createRequestCaptureScope(rid, 'http');
|
|
3882
3917
|
const t0 = requestStartRaw;
|
|
3883
3918
|
const url = req.originalUrl || req.url || '/';
|
|
3884
3919
|
const urlPathOnly = (url || '/').split('?')[0] || '/';
|
|
@@ -3919,7 +3954,7 @@ function reproMiddleware(cfg) {
|
|
|
3919
3954
|
}
|
|
3920
3955
|
return fn();
|
|
3921
3956
|
};
|
|
3922
|
-
runInTrace(() => als.run({ sid, aid, rid, clockSkewMs, excludedSpanIds: new Set(), maskReq }, () => {
|
|
3957
|
+
runInTrace(() => als.run({ sid, aid, rid, clockSkewMs, excludedSpanIds: new Set(), maskReq, captureScope }, () => {
|
|
3923
3958
|
const events = [];
|
|
3924
3959
|
let endpointTrace = null;
|
|
3925
3960
|
let preferredAppTrace = null;
|
|
@@ -3928,6 +3963,7 @@ function reproMiddleware(cfg) {
|
|
|
3928
3963
|
let flushed = false;
|
|
3929
3964
|
let finished = false;
|
|
3930
3965
|
let finishedAt = null;
|
|
3966
|
+
let finishedAtAligned = null;
|
|
3931
3967
|
let lastEventAt = Date.now();
|
|
3932
3968
|
let idleTimer = null;
|
|
3933
3969
|
let hardStopTimer = null;
|
|
@@ -4190,6 +4226,7 @@ function reproMiddleware(cfg) {
|
|
|
4190
4226
|
t: requestEpochMs,
|
|
4191
4227
|
}]
|
|
4192
4228
|
});
|
|
4229
|
+
captureScope.backendRequestCount += 1;
|
|
4193
4230
|
}
|
|
4194
4231
|
catch {
|
|
4195
4232
|
// never break user code
|
|
@@ -4280,6 +4317,7 @@ function reproMiddleware(cfg) {
|
|
|
4280
4317
|
finished = true;
|
|
4281
4318
|
if (finishedAt === null) {
|
|
4282
4319
|
finishedAt = Date.now();
|
|
4320
|
+
finishedAtAligned = alignTimestamp(finishedAt);
|
|
4283
4321
|
}
|
|
4284
4322
|
if (capturedBody === undefined && chunks.length) {
|
|
4285
4323
|
const buf = Buffer.isBuffer(chunks[0])
|
|
@@ -4304,6 +4342,7 @@ function reproMiddleware(cfg) {
|
|
|
4304
4342
|
: sortTraceEventsChronologically(baseEvents);
|
|
4305
4343
|
const traceBatches = chunkTraceEventsForTransport(orderedEvents, rid, aid);
|
|
4306
4344
|
if (traceBatches.length) {
|
|
4345
|
+
captureScope.traceBatchCount = traceBatches.length;
|
|
4307
4346
|
for (let i = 0; i < traceBatches.length; i++) {
|
|
4308
4347
|
const batch = traceBatches[i];
|
|
4309
4348
|
const traceValues = collectBatchTraceValueEntries(batch, i);
|
|
@@ -4322,6 +4361,17 @@ function reproMiddleware(cfg) {
|
|
|
4322
4361
|
});
|
|
4323
4362
|
}
|
|
4324
4363
|
}
|
|
4364
|
+
else {
|
|
4365
|
+
captureScope.traceBatchCount = 0;
|
|
4366
|
+
}
|
|
4367
|
+
post(cfg, sid, {
|
|
4368
|
+
entries: [{
|
|
4369
|
+
actionId: aid,
|
|
4370
|
+
requestRid: rid,
|
|
4371
|
+
requestCaptureManifest: buildRequestCaptureManifestPayload(captureScope, traceBatches.length, finishedAtAligned),
|
|
4372
|
+
t: finishedAtAligned ?? alignedNow(),
|
|
4373
|
+
}],
|
|
4374
|
+
});
|
|
4325
4375
|
}
|
|
4326
4376
|
catch {
|
|
4327
4377
|
// never break user code
|
|
@@ -4786,6 +4836,12 @@ function dehydrateComplexValue(value) {
|
|
|
4786
4836
|
}
|
|
4787
4837
|
function emitDbQuery(cfg, sid, aid, payload) {
|
|
4788
4838
|
const ctx = getCtx();
|
|
4839
|
+
const spanContext = payload?.spanContext ?? captureSpanContextFromTracer();
|
|
4840
|
+
if (!shouldCaptureDbSpan(spanContext))
|
|
4841
|
+
return;
|
|
4842
|
+
if (ctx.captureScope) {
|
|
4843
|
+
ctx.captureScope.dbChangeCount += 1;
|
|
4844
|
+
}
|
|
4789
4845
|
const runCtx = {
|
|
4790
4846
|
sid: ctx.sid,
|
|
4791
4847
|
aid: ctx.aid,
|
|
@@ -4793,6 +4849,7 @@ function emitDbQuery(cfg, sid, aid, payload) {
|
|
|
4793
4849
|
clockSkewMs: ctx.clockSkewMs,
|
|
4794
4850
|
maskReq: ctx.maskReq,
|
|
4795
4851
|
excludedSpanIds: ctx.excludedSpanIds ? new Set(ctx.excludedSpanIds) : undefined,
|
|
4852
|
+
captureScope: ctx.captureScope,
|
|
4796
4853
|
};
|
|
4797
4854
|
scheduleSdkBackgroundWork(async () => {
|
|
4798
4855
|
await als.run(runCtx, async () => {
|
|
@@ -5575,9 +5632,9 @@ function buildKafkaTraceEntries(actionId, requestRid, events) {
|
|
|
5575
5632
|
t: alignedNow(),
|
|
5576
5633
|
}));
|
|
5577
5634
|
}
|
|
5578
|
-
function scheduleKafkaTraceFlush(cfg, kafkaCtx, actionId, requestRid, rawTraceEvents, maskReq) {
|
|
5635
|
+
function scheduleKafkaTraceFlush(cfg, kafkaCtx, actionId, requestRid, rawTraceEvents, maskReq, captureScope) {
|
|
5579
5636
|
const sid = kafkaCtx.sid;
|
|
5580
|
-
if (!sid
|
|
5637
|
+
if (!sid)
|
|
5581
5638
|
return;
|
|
5582
5639
|
const rawEvents = rawTraceEvents.slice();
|
|
5583
5640
|
scheduleSdkBackgroundWork(async () => {
|
|
@@ -5600,9 +5657,22 @@ function scheduleKafkaTraceFlush(cfg, kafkaCtx, actionId, requestRid, rawTraceEv
|
|
|
5600
5657
|
resolvePrivacy: () => getRuntimePrivacyState(cfg).policy ?? null,
|
|
5601
5658
|
});
|
|
5602
5659
|
const entries = buildKafkaTraceEntries(actionId, requestRid, materializedEvents);
|
|
5660
|
+
if (captureScope) {
|
|
5661
|
+
captureScope.traceBatchCount = entries.length;
|
|
5662
|
+
}
|
|
5603
5663
|
if (entries.length) {
|
|
5604
5664
|
post(cfg, sid, { entries });
|
|
5605
5665
|
}
|
|
5666
|
+
if (captureScope) {
|
|
5667
|
+
post(cfg, sid, {
|
|
5668
|
+
entries: [{
|
|
5669
|
+
actionId: actionId ?? null,
|
|
5670
|
+
requestRid,
|
|
5671
|
+
requestCaptureManifest: buildRequestCaptureManifestPayload(captureScope, entries.length),
|
|
5672
|
+
t: alignedNow(),
|
|
5673
|
+
}],
|
|
5674
|
+
});
|
|
5675
|
+
}
|
|
5606
5676
|
});
|
|
5607
5677
|
}, { priority: 'high' });
|
|
5608
5678
|
}
|
|
@@ -5619,6 +5689,7 @@ function patchKafkaProducerInstance(producer, cfg) {
|
|
|
5619
5689
|
const aid = ctx.aid ?? fallback.aid ?? null;
|
|
5620
5690
|
const span = captureSpanContextFromTracer();
|
|
5621
5691
|
const publishRid = nextRequestId(alignedNow());
|
|
5692
|
+
const captureScope = createRequestCaptureScope(publishRid, 'kafka');
|
|
5622
5693
|
const payloadObj = payload && typeof payload === 'object' ? payload : {};
|
|
5623
5694
|
const topic = sanitizeKafkaTopic(payloadObj.topic);
|
|
5624
5695
|
const rawMessages = Array.isArray(payloadObj.messages) ? payloadObj.messages : [];
|
|
@@ -5705,6 +5776,15 @@ function patchKafkaProducerInstance(producer, cfg) {
|
|
|
5705
5776
|
},
|
|
5706
5777
|
],
|
|
5707
5778
|
});
|
|
5779
|
+
captureScope.backendRequestCount += 1;
|
|
5780
|
+
post(cfg, sid, {
|
|
5781
|
+
entries: [{
|
|
5782
|
+
actionId: aid ?? null,
|
|
5783
|
+
requestRid: publishRid,
|
|
5784
|
+
requestCaptureManifest: buildRequestCaptureManifestPayload(captureScope, 0),
|
|
5785
|
+
t: alignedNow(),
|
|
5786
|
+
}],
|
|
5787
|
+
});
|
|
5708
5788
|
}
|
|
5709
5789
|
};
|
|
5710
5790
|
}
|
|
@@ -5717,6 +5797,7 @@ function patchKafkaProducerInstance(producer, cfg) {
|
|
|
5717
5797
|
const aid = ctx.aid ?? fallback.aid ?? null;
|
|
5718
5798
|
const span = captureSpanContextFromTracer();
|
|
5719
5799
|
const publishRid = nextRequestId(alignedNow());
|
|
5800
|
+
const captureScope = createRequestCaptureScope(publishRid, 'kafka');
|
|
5720
5801
|
const payloadObj = payload && typeof payload === 'object' ? payload : {};
|
|
5721
5802
|
const topicMessages = Array.isArray(payloadObj.topicMessages)
|
|
5722
5803
|
? payloadObj.topicMessages
|
|
@@ -5834,6 +5915,15 @@ function patchKafkaProducerInstance(producer, cfg) {
|
|
|
5834
5915
|
},
|
|
5835
5916
|
],
|
|
5836
5917
|
});
|
|
5918
|
+
captureScope.backendRequestCount += 1;
|
|
5919
|
+
post(cfg, sid, {
|
|
5920
|
+
entries: [{
|
|
5921
|
+
actionId: aid ?? null,
|
|
5922
|
+
requestRid: publishRid,
|
|
5923
|
+
requestCaptureManifest: buildRequestCaptureManifestPayload(captureScope, 0),
|
|
5924
|
+
t: alignedNow(),
|
|
5925
|
+
}],
|
|
5926
|
+
});
|
|
5837
5927
|
}
|
|
5838
5928
|
};
|
|
5839
5929
|
}
|
|
@@ -5848,6 +5938,7 @@ function wrapKafkaEachMessageHandler(eachMessage, cfg, consumer) {
|
|
|
5848
5938
|
const aid = runtime.aid;
|
|
5849
5939
|
const consumeRid = nextRequestId(`${topic}-${message?.offset ?? alignedNow()}`);
|
|
5850
5940
|
const traceId = consumeRid;
|
|
5941
|
+
const captureScope = createRequestCaptureScope(consumeRid, 'kafka');
|
|
5851
5942
|
const rawTraceEvents = [];
|
|
5852
5943
|
const maskReq = {
|
|
5853
5944
|
method: 'KAFKA_CONSUME',
|
|
@@ -5862,6 +5953,7 @@ function wrapKafkaEachMessageHandler(eachMessage, cfg, consumer) {
|
|
|
5862
5953
|
excludedSpanIds: new Set(),
|
|
5863
5954
|
maskReq,
|
|
5864
5955
|
privacyTasks: new Set(),
|
|
5956
|
+
captureScope,
|
|
5865
5957
|
};
|
|
5866
5958
|
let unsubscribe;
|
|
5867
5959
|
const invoke = async () => {
|
|
@@ -5944,7 +6036,8 @@ function wrapKafkaEachMessageHandler(eachMessage, cfg, consumer) {
|
|
|
5944
6036
|
t: alignedNow(),
|
|
5945
6037
|
}],
|
|
5946
6038
|
});
|
|
5947
|
-
|
|
6039
|
+
captureScope.backendRequestCount += 1;
|
|
6040
|
+
scheduleKafkaTraceFlush(cfg, kafkaCtx, aid, consumeRid, rawTraceEvents, maskReq, captureScope);
|
|
5948
6041
|
}
|
|
5949
6042
|
};
|
|
5950
6043
|
}
|
|
@@ -5959,6 +6052,7 @@ function wrapKafkaEachBatchHandler(eachBatch, cfg, consumer) {
|
|
|
5959
6052
|
const aid = runtime.aid;
|
|
5960
6053
|
const consumeRid = nextRequestId(`${topic}-batch-${alignedNow()}`);
|
|
5961
6054
|
const traceId = consumeRid;
|
|
6055
|
+
const captureScope = createRequestCaptureScope(consumeRid, 'kafka');
|
|
5962
6056
|
const rawTraceEvents = [];
|
|
5963
6057
|
const maskReq = {
|
|
5964
6058
|
method: 'KAFKA_CONSUME_BATCH',
|
|
@@ -5973,6 +6067,7 @@ function wrapKafkaEachBatchHandler(eachBatch, cfg, consumer) {
|
|
|
5973
6067
|
excludedSpanIds: new Set(),
|
|
5974
6068
|
maskReq,
|
|
5975
6069
|
privacyTasks: new Set(),
|
|
6070
|
+
captureScope,
|
|
5976
6071
|
};
|
|
5977
6072
|
let unsubscribe;
|
|
5978
6073
|
const invoke = async () => {
|
|
@@ -6053,7 +6148,8 @@ function wrapKafkaEachBatchHandler(eachBatch, cfg, consumer) {
|
|
|
6053
6148
|
t: alignedNow(),
|
|
6054
6149
|
}],
|
|
6055
6150
|
});
|
|
6056
|
-
|
|
6151
|
+
captureScope.backendRequestCount += 1;
|
|
6152
|
+
scheduleKafkaTraceFlush(cfg, kafkaCtx, aid, consumeRid, rawTraceEvents, maskReq, captureScope);
|
|
6057
6153
|
}
|
|
6058
6154
|
};
|
|
6059
6155
|
}
|
|
@@ -6705,10 +6801,14 @@ function patchSendgridFromModuleExports(sgMail, cfg) {
|
|
|
6705
6801
|
const aid = ctx.aid ?? cfg.resolveContext?.()?.aid;
|
|
6706
6802
|
if (!sid)
|
|
6707
6803
|
return;
|
|
6804
|
+
if (ctx.captureScope) {
|
|
6805
|
+
ctx.captureScope.emailEventCount += 1;
|
|
6806
|
+
}
|
|
6708
6807
|
const norm = normalizeSendgridMessage(rawMsg);
|
|
6709
6808
|
post(cfg, sid, {
|
|
6710
6809
|
entries: [{
|
|
6711
6810
|
actionId: aid ?? null,
|
|
6811
|
+
requestRid: ctx.rid ?? null,
|
|
6712
6812
|
email: {
|
|
6713
6813
|
provider: 'sendgrid',
|
|
6714
6814
|
kind,
|
package/dist/ingest/client.js
CHANGED
|
@@ -14,6 +14,13 @@ const DEFAULT_QUEUE_MAX_ITEMS = (() => {
|
|
|
14
14
|
const DEFAULT_QUEUE_MAX_ATTEMPTS = 5;
|
|
15
15
|
const DEFAULT_QUEUE_BACKOFF_MAX_MS = 4000;
|
|
16
16
|
const DEFAULT_DEFER_DRAIN_MS = 25;
|
|
17
|
+
const DEFAULT_INGEST_REQUEST_TIMEOUT_MS = (() => {
|
|
18
|
+
const env = Number(typeof process !== 'undefined' ? process?.env?.REPRO_INGEST_REQUEST_TIMEOUT_MS : undefined);
|
|
19
|
+
if (Number.isFinite(env) && env >= 0) {
|
|
20
|
+
return Math.trunc(env);
|
|
21
|
+
}
|
|
22
|
+
return 5000;
|
|
23
|
+
})();
|
|
17
24
|
const ingestQueue = [];
|
|
18
25
|
let drainTimer = null;
|
|
19
26
|
let draining = false;
|
|
@@ -65,11 +72,29 @@ const sendIngestEntries = async (config, sessionId, entries) => {
|
|
|
65
72
|
if (!events.length) {
|
|
66
73
|
return true;
|
|
67
74
|
}
|
|
75
|
+
let timeout = null;
|
|
76
|
+
const Controller = globalThis.AbortController;
|
|
77
|
+
const controller = DEFAULT_INGEST_REQUEST_TIMEOUT_MS > 0 && typeof Controller === 'function'
|
|
78
|
+
? new Controller()
|
|
79
|
+
: null;
|
|
68
80
|
try {
|
|
81
|
+
if (controller) {
|
|
82
|
+
timeout = setTimeout(() => {
|
|
83
|
+
try {
|
|
84
|
+
controller.abort();
|
|
85
|
+
}
|
|
86
|
+
catch { }
|
|
87
|
+
}, DEFAULT_INGEST_REQUEST_TIMEOUT_MS);
|
|
88
|
+
try {
|
|
89
|
+
timeout.unref?.();
|
|
90
|
+
}
|
|
91
|
+
catch { }
|
|
92
|
+
}
|
|
69
93
|
const response = await fetch(resolveIngestUrl(config), {
|
|
70
94
|
method: 'POST',
|
|
71
95
|
headers: toHeaders(config),
|
|
72
96
|
body: JSON.stringify({ events }),
|
|
97
|
+
...(controller ? { signal: controller.signal } : {}),
|
|
73
98
|
});
|
|
74
99
|
return response.ok;
|
|
75
100
|
}
|
|
@@ -77,6 +102,14 @@ const sendIngestEntries = async (config, sessionId, entries) => {
|
|
|
77
102
|
// swallow in SDK
|
|
78
103
|
return false;
|
|
79
104
|
}
|
|
105
|
+
finally {
|
|
106
|
+
if (timeout) {
|
|
107
|
+
try {
|
|
108
|
+
clearTimeout(timeout);
|
|
109
|
+
}
|
|
110
|
+
catch { }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
80
113
|
};
|
|
81
114
|
const scheduleDrain = (delayMs = 0) => {
|
|
82
115
|
if (drainTimer) {
|
package/dist/ingest/mapper.js
CHANGED
|
@@ -6,6 +6,10 @@ const toIso = (timestamp) => {
|
|
|
6
6
|
return new Date(numeric).toISOString();
|
|
7
7
|
};
|
|
8
8
|
const inferEventType = (entry) => {
|
|
9
|
+
if (entry.requestCaptureManifest !== undefined)
|
|
10
|
+
return 'request_capture_manifest';
|
|
11
|
+
if (entry.sessionCaptureManifest !== undefined)
|
|
12
|
+
return 'session_capture_manifest';
|
|
9
13
|
if (entry.request !== undefined)
|
|
10
14
|
return 'backend_request';
|
|
11
15
|
if (entry.db !== undefined)
|
package/dist/ingest/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reproapp/node-sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
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/unawaited.test.js && node test/integration-unawaited.js && node test/request-flush-timing.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/ingest-queue-retention.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/privacy-runtime-policy.test.js && node test/runtime-privacy-materialization.test.js && node test/kafka-runtime-privacy-policy.test.js"
|
|
15
|
+
"test": "npm run build && node test/unawaited.test.js && node test/integration-unawaited.js && node test/request-flush-timing.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/ingest-queue-retention.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/privacy-runtime-policy.test.js && node test/runtime-privacy-materialization.test.js && node test/kafka-runtime-privacy-policy.test.js"
|
|
16
16
|
},
|
|
17
17
|
"peerDependencies": {
|
|
18
18
|
"express": "^5.1.0",
|
package/src/index.ts
CHANGED
|
@@ -1152,7 +1152,63 @@ type Ctx = {
|
|
|
1152
1152
|
excludedSpanIds?: Set<string>;
|
|
1153
1153
|
maskReq?: MaskRequestContext;
|
|
1154
1154
|
privacyTasks?: Set<Promise<void>>;
|
|
1155
|
+
captureScope?: RequestCaptureScopeState;
|
|
1155
1156
|
};
|
|
1157
|
+
|
|
1158
|
+
type RequestCaptureScopeState = {
|
|
1159
|
+
requestRid: string;
|
|
1160
|
+
transport: 'http' | 'kafka' | 'background';
|
|
1161
|
+
backendRequestCount: number;
|
|
1162
|
+
traceBatchCount: number;
|
|
1163
|
+
dbChangeCount: number;
|
|
1164
|
+
emailEventCount: number;
|
|
1165
|
+
};
|
|
1166
|
+
|
|
1167
|
+
function createRequestCaptureScope(
|
|
1168
|
+
requestRid: string,
|
|
1169
|
+
transport: RequestCaptureScopeState['transport'],
|
|
1170
|
+
): RequestCaptureScopeState {
|
|
1171
|
+
return {
|
|
1172
|
+
requestRid,
|
|
1173
|
+
transport,
|
|
1174
|
+
backendRequestCount: 0,
|
|
1175
|
+
traceBatchCount: 0,
|
|
1176
|
+
dbChangeCount: 0,
|
|
1177
|
+
emailEventCount: 0,
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function buildRequestCaptureManifestPayload(
|
|
1182
|
+
scope: RequestCaptureScopeState,
|
|
1183
|
+
traceBatchTotal: number,
|
|
1184
|
+
closedAtMs?: number | null,
|
|
1185
|
+
): Record<string, any> {
|
|
1186
|
+
const expectedTraceBatches = Math.max(0, Math.trunc(traceBatchTotal));
|
|
1187
|
+
const closedAt =
|
|
1188
|
+
typeof closedAtMs === 'number' && Number.isFinite(closedAtMs)
|
|
1189
|
+
? closedAtMs
|
|
1190
|
+
: alignedNow();
|
|
1191
|
+
return {
|
|
1192
|
+
scopeId: `req:${scope.requestRid}`,
|
|
1193
|
+
requestRid: scope.requestRid,
|
|
1194
|
+
transport: scope.transport,
|
|
1195
|
+
expected: {
|
|
1196
|
+
backendRequest: scope.backendRequestCount,
|
|
1197
|
+
traceBatch: expectedTraceBatches,
|
|
1198
|
+
dbChange: scope.dbChangeCount,
|
|
1199
|
+
emailEvent: scope.emailEventCount,
|
|
1200
|
+
},
|
|
1201
|
+
traceBatch:
|
|
1202
|
+
expectedTraceBatches > 0
|
|
1203
|
+
? {
|
|
1204
|
+
from: 0,
|
|
1205
|
+
to: expectedTraceBatches - 1,
|
|
1206
|
+
}
|
|
1207
|
+
: undefined,
|
|
1208
|
+
closedAt,
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1156
1212
|
const als = new AsyncLocalStorage<Ctx>();
|
|
1157
1213
|
const getCtx = () => als.getStore() || {};
|
|
1158
1214
|
|
|
@@ -4747,6 +4803,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
4747
4803
|
const clockSkewMs = headerTs !== null ? headerTs - requestStartRaw : 0;
|
|
4748
4804
|
const requestEpochMs = headerTs !== null ? headerTs : requestStartRaw + clockSkewMs;
|
|
4749
4805
|
const rid = nextRequestId(requestEpochMs);
|
|
4806
|
+
const captureScope = createRequestCaptureScope(rid, 'http');
|
|
4750
4807
|
const t0 = requestStartRaw;
|
|
4751
4808
|
const url = (req as any).originalUrl || req.url || '/';
|
|
4752
4809
|
const urlPathOnly = (url || '/').split('?')[0] || '/';
|
|
@@ -4784,7 +4841,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
4784
4841
|
return fn();
|
|
4785
4842
|
};
|
|
4786
4843
|
|
|
4787
|
-
|
|
4844
|
+
runInTrace(() => als.run({ sid, aid, rid, clockSkewMs, excludedSpanIds: new Set<string>(), maskReq, captureScope }, () => {
|
|
4788
4845
|
const events: PendingTraceEventRecord[] = [];
|
|
4789
4846
|
let endpointTrace: EndpointTraceInfo | null = null;
|
|
4790
4847
|
let preferredAppTrace: EndpointTraceInfo | null = null;
|
|
@@ -4793,15 +4850,16 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
4793
4850
|
let flushed = false;
|
|
4794
4851
|
let finished = false;
|
|
4795
4852
|
let finishedAt: number | null = null;
|
|
4853
|
+
let finishedAtAligned: number | null = null;
|
|
4796
4854
|
let lastEventAt: number = Date.now();
|
|
4797
4855
|
let idleTimer: NodeJS.Timeout | null = null;
|
|
4798
|
-
|
|
4856
|
+
let hardStopTimer: NodeJS.Timeout | null = null;
|
|
4799
4857
|
let flushPayload: null | (() => Promise<void>) = null;
|
|
4800
4858
|
let requestCaptureScheduled = false;
|
|
4801
4859
|
let sessionDrainWait: Promise<void> | null = null;
|
|
4802
4860
|
const activeSpans = new Set<string>();
|
|
4803
4861
|
let anonymousSpanDepth = 0;
|
|
4804
|
-
|
|
4862
|
+
const ACTIVE_SPAN_FORCE_FLUSH_MS = 60000; // hard cutoff after finish to avoid endlessly running replies
|
|
4805
4863
|
|
|
4806
4864
|
const clearTimers = () => {
|
|
4807
4865
|
if (idleTimer) {
|
|
@@ -5086,6 +5144,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
5086
5144
|
t: requestEpochMs,
|
|
5087
5145
|
}]
|
|
5088
5146
|
});
|
|
5147
|
+
captureScope.backendRequestCount += 1;
|
|
5089
5148
|
} catch {
|
|
5090
5149
|
// never break user code
|
|
5091
5150
|
}
|
|
@@ -5178,6 +5237,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
5178
5237
|
finished = true;
|
|
5179
5238
|
if (finishedAt === null) {
|
|
5180
5239
|
finishedAt = Date.now();
|
|
5240
|
+
finishedAtAligned = alignTimestamp(finishedAt);
|
|
5181
5241
|
}
|
|
5182
5242
|
if (capturedBody === undefined && chunks.length) {
|
|
5183
5243
|
const buf = Buffer.isBuffer(chunks[0])
|
|
@@ -5190,47 +5250,63 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
5190
5250
|
|
|
5191
5251
|
if (!flushPayload) {
|
|
5192
5252
|
flushPayload = async () => {
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
}
|
|
5229
|
-
}
|
|
5253
|
+
try {
|
|
5254
|
+
const pendingEvents = preparePendingTraceEventsForFlush(events.slice());
|
|
5255
|
+
const materializedEvents = await materializePendingTraceEventsAsync(pendingEvents, {
|
|
5256
|
+
cfg,
|
|
5257
|
+
maskReq,
|
|
5258
|
+
masking,
|
|
5259
|
+
resolvePrivacy,
|
|
5260
|
+
});
|
|
5261
|
+
const baseEvents = balanceTraceEvents(materializedEvents.slice());
|
|
5262
|
+
const orderedEvents = TRACE_ORDER_MODE === 'tree'
|
|
5263
|
+
? reorderTraceEvents(baseEvents)
|
|
5264
|
+
: sortTraceEventsChronologically(baseEvents);
|
|
5265
|
+
const traceBatches = chunkTraceEventsForTransport(
|
|
5266
|
+
orderedEvents,
|
|
5267
|
+
rid,
|
|
5268
|
+
aid,
|
|
5269
|
+
);
|
|
5270
|
+
|
|
5271
|
+
if (traceBatches.length) {
|
|
5272
|
+
captureScope.traceBatchCount = traceBatches.length;
|
|
5273
|
+
for (let i = 0; i < traceBatches.length; i++) {
|
|
5274
|
+
const batch = traceBatches[i];
|
|
5275
|
+
const traceValues = collectBatchTraceValueEntries(batch, i);
|
|
5276
|
+
|
|
5277
|
+
post(cfg, sid, {
|
|
5278
|
+
entries: [{
|
|
5279
|
+
actionId: aid,
|
|
5280
|
+
trace: batch,
|
|
5281
|
+
traceValues: traceValues.length ? traceValues : undefined,
|
|
5282
|
+
traceBatch: {
|
|
5283
|
+
rid,
|
|
5284
|
+
index: i,
|
|
5285
|
+
total: traceBatches.length,
|
|
5286
|
+
},
|
|
5287
|
+
t: alignedNow(),
|
|
5288
|
+
}],
|
|
5289
|
+
});
|
|
5230
5290
|
}
|
|
5231
|
-
}
|
|
5232
|
-
|
|
5291
|
+
} else {
|
|
5292
|
+
captureScope.traceBatchCount = 0;
|
|
5233
5293
|
}
|
|
5294
|
+
|
|
5295
|
+
post(cfg, sid, {
|
|
5296
|
+
entries: [{
|
|
5297
|
+
actionId: aid,
|
|
5298
|
+
requestRid: rid,
|
|
5299
|
+
requestCaptureManifest: buildRequestCaptureManifestPayload(
|
|
5300
|
+
captureScope,
|
|
5301
|
+
traceBatches.length,
|
|
5302
|
+
finishedAtAligned,
|
|
5303
|
+
),
|
|
5304
|
+
t: finishedAtAligned ?? alignedNow(),
|
|
5305
|
+
}],
|
|
5306
|
+
});
|
|
5307
|
+
} catch {
|
|
5308
|
+
// never break user code
|
|
5309
|
+
}
|
|
5234
5310
|
};
|
|
5235
5311
|
}
|
|
5236
5312
|
|
|
@@ -5697,6 +5773,11 @@ function dehydrateComplexValue(value: any) {
|
|
|
5697
5773
|
|
|
5698
5774
|
function emitDbQuery(cfg: any, sid?: string, aid?: string, payload?: any) {
|
|
5699
5775
|
const ctx = getCtx() as Ctx;
|
|
5776
|
+
const spanContext = payload?.spanContext ?? captureSpanContextFromTracer();
|
|
5777
|
+
if (!shouldCaptureDbSpan(spanContext)) return;
|
|
5778
|
+
if (ctx.captureScope) {
|
|
5779
|
+
ctx.captureScope.dbChangeCount += 1;
|
|
5780
|
+
}
|
|
5700
5781
|
const runCtx: Ctx = {
|
|
5701
5782
|
sid: ctx.sid,
|
|
5702
5783
|
aid: ctx.aid,
|
|
@@ -5704,6 +5785,7 @@ function emitDbQuery(cfg: any, sid?: string, aid?: string, payload?: any) {
|
|
|
5704
5785
|
clockSkewMs: ctx.clockSkewMs,
|
|
5705
5786
|
maskReq: ctx.maskReq,
|
|
5706
5787
|
excludedSpanIds: ctx.excludedSpanIds ? new Set(ctx.excludedSpanIds) : undefined,
|
|
5788
|
+
captureScope: ctx.captureScope,
|
|
5707
5789
|
};
|
|
5708
5790
|
scheduleSdkBackgroundWork(async () => {
|
|
5709
5791
|
await als.run(runCtx, async () => {
|
|
@@ -6588,9 +6670,10 @@ function scheduleKafkaTraceFlush(
|
|
|
6588
6670
|
requestRid: string,
|
|
6589
6671
|
rawTraceEvents: any[],
|
|
6590
6672
|
maskReq: MaskRequestContext,
|
|
6673
|
+
captureScope?: RequestCaptureScopeState,
|
|
6591
6674
|
): void {
|
|
6592
6675
|
const sid = kafkaCtx.sid;
|
|
6593
|
-
if (!sid
|
|
6676
|
+
if (!sid) return;
|
|
6594
6677
|
const rawEvents = rawTraceEvents.slice();
|
|
6595
6678
|
|
|
6596
6679
|
scheduleSdkBackgroundWork(async () => {
|
|
@@ -6611,9 +6694,25 @@ function scheduleKafkaTraceFlush(
|
|
|
6611
6694
|
resolvePrivacy: () => getRuntimePrivacyState(cfg).policy ?? null,
|
|
6612
6695
|
});
|
|
6613
6696
|
const entries = buildKafkaTraceEntries(actionId, requestRid, materializedEvents);
|
|
6697
|
+
if (captureScope) {
|
|
6698
|
+
captureScope.traceBatchCount = entries.length;
|
|
6699
|
+
}
|
|
6614
6700
|
if (entries.length) {
|
|
6615
6701
|
post(cfg, sid, { entries });
|
|
6616
6702
|
}
|
|
6703
|
+
if (captureScope) {
|
|
6704
|
+
post(cfg, sid, {
|
|
6705
|
+
entries: [{
|
|
6706
|
+
actionId: actionId ?? null,
|
|
6707
|
+
requestRid,
|
|
6708
|
+
requestCaptureManifest: buildRequestCaptureManifestPayload(
|
|
6709
|
+
captureScope,
|
|
6710
|
+
entries.length,
|
|
6711
|
+
),
|
|
6712
|
+
t: alignedNow(),
|
|
6713
|
+
}],
|
|
6714
|
+
});
|
|
6715
|
+
}
|
|
6617
6716
|
});
|
|
6618
6717
|
}, { priority: 'high' });
|
|
6619
6718
|
}
|
|
@@ -6631,6 +6730,7 @@ function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
|
|
|
6631
6730
|
const aid = ctx.aid ?? fallback.aid ?? null;
|
|
6632
6731
|
const span = captureSpanContextFromTracer();
|
|
6633
6732
|
const publishRid = nextRequestId(alignedNow());
|
|
6733
|
+
const captureScope = createRequestCaptureScope(publishRid, 'kafka');
|
|
6634
6734
|
const payloadObj = payload && typeof payload === 'object' ? payload : {};
|
|
6635
6735
|
const topic = sanitizeKafkaTopic((payloadObj as any).topic);
|
|
6636
6736
|
const rawMessages = Array.isArray((payloadObj as any).messages) ? (payloadObj as any).messages : [];
|
|
@@ -6719,6 +6819,18 @@ function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
|
|
|
6719
6819
|
},
|
|
6720
6820
|
],
|
|
6721
6821
|
});
|
|
6822
|
+
captureScope.backendRequestCount += 1;
|
|
6823
|
+
post(cfg, sid, {
|
|
6824
|
+
entries: [{
|
|
6825
|
+
actionId: aid ?? null,
|
|
6826
|
+
requestRid: publishRid,
|
|
6827
|
+
requestCaptureManifest: buildRequestCaptureManifestPayload(
|
|
6828
|
+
captureScope,
|
|
6829
|
+
0,
|
|
6830
|
+
),
|
|
6831
|
+
t: alignedNow(),
|
|
6832
|
+
}],
|
|
6833
|
+
});
|
|
6722
6834
|
}
|
|
6723
6835
|
};
|
|
6724
6836
|
}
|
|
@@ -6732,6 +6844,7 @@ function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
|
|
|
6732
6844
|
const aid = ctx.aid ?? fallback.aid ?? null;
|
|
6733
6845
|
const span = captureSpanContextFromTracer();
|
|
6734
6846
|
const publishRid = nextRequestId(alignedNow());
|
|
6847
|
+
const captureScope = createRequestCaptureScope(publishRid, 'kafka');
|
|
6735
6848
|
|
|
6736
6849
|
const payloadObj = payload && typeof payload === 'object' ? payload : {};
|
|
6737
6850
|
const topicMessages = Array.isArray((payloadObj as any).topicMessages)
|
|
@@ -6854,6 +6967,18 @@ function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
|
|
|
6854
6967
|
},
|
|
6855
6968
|
],
|
|
6856
6969
|
});
|
|
6970
|
+
captureScope.backendRequestCount += 1;
|
|
6971
|
+
post(cfg, sid, {
|
|
6972
|
+
entries: [{
|
|
6973
|
+
actionId: aid ?? null,
|
|
6974
|
+
requestRid: publishRid,
|
|
6975
|
+
requestCaptureManifest: buildRequestCaptureManifestPayload(
|
|
6976
|
+
captureScope,
|
|
6977
|
+
0,
|
|
6978
|
+
),
|
|
6979
|
+
t: alignedNow(),
|
|
6980
|
+
}],
|
|
6981
|
+
});
|
|
6857
6982
|
}
|
|
6858
6983
|
};
|
|
6859
6984
|
}
|
|
@@ -6869,6 +6994,7 @@ function wrapKafkaEachMessageHandler(eachMessage: any, cfg: KafkaJsPatchConfig,
|
|
|
6869
6994
|
const aid = runtime.aid;
|
|
6870
6995
|
const consumeRid = nextRequestId(`${topic}-${message?.offset ?? alignedNow()}`);
|
|
6871
6996
|
const traceId = consumeRid;
|
|
6997
|
+
const captureScope = createRequestCaptureScope(consumeRid, 'kafka');
|
|
6872
6998
|
const rawTraceEvents: any[] = [];
|
|
6873
6999
|
const maskReq: MaskRequestContext = {
|
|
6874
7000
|
method: 'KAFKA_CONSUME',
|
|
@@ -6883,6 +7009,7 @@ function wrapKafkaEachMessageHandler(eachMessage: any, cfg: KafkaJsPatchConfig,
|
|
|
6883
7009
|
excludedSpanIds: new Set<string>(),
|
|
6884
7010
|
maskReq,
|
|
6885
7011
|
privacyTasks: new Set<Promise<void>>(),
|
|
7012
|
+
captureScope,
|
|
6886
7013
|
};
|
|
6887
7014
|
let unsubscribe: undefined | (() => void);
|
|
6888
7015
|
|
|
@@ -6960,7 +7087,8 @@ function wrapKafkaEachMessageHandler(eachMessage: any, cfg: KafkaJsPatchConfig,
|
|
|
6960
7087
|
t: alignedNow(),
|
|
6961
7088
|
}],
|
|
6962
7089
|
});
|
|
6963
|
-
|
|
7090
|
+
captureScope.backendRequestCount += 1;
|
|
7091
|
+
scheduleKafkaTraceFlush(cfg, kafkaCtx, aid, consumeRid, rawTraceEvents, maskReq, captureScope);
|
|
6964
7092
|
}
|
|
6965
7093
|
};
|
|
6966
7094
|
}
|
|
@@ -6976,6 +7104,7 @@ function wrapKafkaEachBatchHandler(eachBatch: any, cfg: KafkaJsPatchConfig, cons
|
|
|
6976
7104
|
const aid = runtime.aid;
|
|
6977
7105
|
const consumeRid = nextRequestId(`${topic}-batch-${alignedNow()}`);
|
|
6978
7106
|
const traceId = consumeRid;
|
|
7107
|
+
const captureScope = createRequestCaptureScope(consumeRid, 'kafka');
|
|
6979
7108
|
const rawTraceEvents: any[] = [];
|
|
6980
7109
|
const maskReq: MaskRequestContext = {
|
|
6981
7110
|
method: 'KAFKA_CONSUME_BATCH',
|
|
@@ -6990,6 +7119,7 @@ function wrapKafkaEachBatchHandler(eachBatch: any, cfg: KafkaJsPatchConfig, cons
|
|
|
6990
7119
|
excludedSpanIds: new Set<string>(),
|
|
6991
7120
|
maskReq,
|
|
6992
7121
|
privacyTasks: new Set<Promise<void>>(),
|
|
7122
|
+
captureScope,
|
|
6993
7123
|
};
|
|
6994
7124
|
let unsubscribe: undefined | (() => void);
|
|
6995
7125
|
|
|
@@ -7065,7 +7195,8 @@ function wrapKafkaEachBatchHandler(eachBatch: any, cfg: KafkaJsPatchConfig, cons
|
|
|
7065
7195
|
t: alignedNow(),
|
|
7066
7196
|
}],
|
|
7067
7197
|
});
|
|
7068
|
-
|
|
7198
|
+
captureScope.backendRequestCount += 1;
|
|
7199
|
+
scheduleKafkaTraceFlush(cfg, kafkaCtx, aid, consumeRid, rawTraceEvents, maskReq, captureScope);
|
|
7069
7200
|
}
|
|
7070
7201
|
};
|
|
7071
7202
|
}
|
|
@@ -7761,11 +7892,15 @@ function patchSendgridFromModuleExports(sgMail: any, cfg: SendgridPatchConfig) {
|
|
|
7761
7892
|
const sid = ctx.sid ?? cfg.resolveContext?.()?.sid;
|
|
7762
7893
|
const aid = ctx.aid ?? cfg.resolveContext?.()?.aid;
|
|
7763
7894
|
if (!sid) return;
|
|
7895
|
+
if (ctx.captureScope) {
|
|
7896
|
+
ctx.captureScope.emailEventCount += 1;
|
|
7897
|
+
}
|
|
7764
7898
|
|
|
7765
7899
|
const norm = normalizeSendgridMessage(rawMsg);
|
|
7766
7900
|
post(cfg, sid, {
|
|
7767
7901
|
entries: [{
|
|
7768
7902
|
actionId: aid ?? null,
|
|
7903
|
+
requestRid: ctx.rid ?? null,
|
|
7769
7904
|
email: {
|
|
7770
7905
|
provider: 'sendgrid',
|
|
7771
7906
|
kind,
|
package/src/ingest/client.ts
CHANGED
|
@@ -13,6 +13,13 @@ const DEFAULT_QUEUE_MAX_ITEMS = (() => {
|
|
|
13
13
|
const DEFAULT_QUEUE_MAX_ATTEMPTS = 5;
|
|
14
14
|
const DEFAULT_QUEUE_BACKOFF_MAX_MS = 4000;
|
|
15
15
|
const DEFAULT_DEFER_DRAIN_MS = 25;
|
|
16
|
+
const DEFAULT_INGEST_REQUEST_TIMEOUT_MS = (() => {
|
|
17
|
+
const env = Number(typeof process !== 'undefined' ? (process as any)?.env?.REPRO_INGEST_REQUEST_TIMEOUT_MS : undefined);
|
|
18
|
+
if (Number.isFinite(env) && env >= 0) {
|
|
19
|
+
return Math.trunc(env);
|
|
20
|
+
}
|
|
21
|
+
return 5000;
|
|
22
|
+
})();
|
|
16
23
|
|
|
17
24
|
type QueuedIngestBatch = {
|
|
18
25
|
config: IngestClientConfig;
|
|
@@ -91,16 +98,41 @@ const sendIngestEntries = async (
|
|
|
91
98
|
return true;
|
|
92
99
|
}
|
|
93
100
|
|
|
101
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
102
|
+
const Controller = (globalThis as any).AbortController;
|
|
103
|
+
const controller =
|
|
104
|
+
DEFAULT_INGEST_REQUEST_TIMEOUT_MS > 0 && typeof Controller === 'function'
|
|
105
|
+
? new Controller()
|
|
106
|
+
: null;
|
|
107
|
+
|
|
94
108
|
try {
|
|
109
|
+
if (controller) {
|
|
110
|
+
timeout = setTimeout(() => {
|
|
111
|
+
try {
|
|
112
|
+
controller.abort();
|
|
113
|
+
} catch {}
|
|
114
|
+
}, DEFAULT_INGEST_REQUEST_TIMEOUT_MS);
|
|
115
|
+
try {
|
|
116
|
+
(timeout as any).unref?.();
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
|
|
95
120
|
const response = await fetch(resolveIngestUrl(config), {
|
|
96
121
|
method: 'POST',
|
|
97
122
|
headers: toHeaders(config),
|
|
98
123
|
body: JSON.stringify({ events }),
|
|
124
|
+
...(controller ? { signal: controller.signal } : {}),
|
|
99
125
|
});
|
|
100
126
|
return response.ok;
|
|
101
127
|
} catch {
|
|
102
128
|
// swallow in SDK
|
|
103
129
|
return false;
|
|
130
|
+
} finally {
|
|
131
|
+
if (timeout) {
|
|
132
|
+
try {
|
|
133
|
+
clearTimeout(timeout);
|
|
134
|
+
} catch {}
|
|
135
|
+
}
|
|
104
136
|
}
|
|
105
137
|
};
|
|
106
138
|
|
package/src/ingest/mapper.ts
CHANGED
|
@@ -7,6 +7,8 @@ const toIso = (timestamp: unknown): string => {
|
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
const inferEventType = (entry: LegacyEntry): string => {
|
|
10
|
+
if (entry.requestCaptureManifest !== undefined) return 'request_capture_manifest';
|
|
11
|
+
if (entry.sessionCaptureManifest !== undefined) return 'session_capture_manifest';
|
|
10
12
|
if (entry.request !== undefined) return 'backend_request';
|
|
11
13
|
if (entry.db !== undefined) return 'db_change';
|
|
12
14
|
if (entry.trace !== undefined) return 'trace_batch';
|
package/src/ingest/types.ts
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
process.env.REPRO_INGEST_REQUEST_TIMEOUT_MS = '25';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
|
|
5
|
+
const { postIngestEntries } = require('../dist/ingest/client');
|
|
6
|
+
|
|
7
|
+
async function main() {
|
|
8
|
+
const originalFetch = global.fetch;
|
|
9
|
+
let aborted = false;
|
|
10
|
+
|
|
11
|
+
global.fetch = async (_url, init = {}) => {
|
|
12
|
+
return await new Promise((_resolve, reject) => {
|
|
13
|
+
const signal = init.signal;
|
|
14
|
+
assert(signal, 'fetch should receive an AbortSignal');
|
|
15
|
+
const keepAlive = setTimeout(() => {}, 1000);
|
|
16
|
+
signal.addEventListener('abort', () => {
|
|
17
|
+
clearTimeout(keepAlive);
|
|
18
|
+
aborted = true;
|
|
19
|
+
const error = new Error('aborted');
|
|
20
|
+
error.name = 'AbortError';
|
|
21
|
+
reject(error);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const startedAt = Date.now();
|
|
28
|
+
await postIngestEntries(
|
|
29
|
+
{
|
|
30
|
+
tenantId: 'TENANT_test',
|
|
31
|
+
appId: 'APP_test',
|
|
32
|
+
appSecret: 'secret',
|
|
33
|
+
ingestBase: 'http://127.0.0.1:65535',
|
|
34
|
+
},
|
|
35
|
+
'S_ingest_timeout',
|
|
36
|
+
[
|
|
37
|
+
{
|
|
38
|
+
actionId: 'A_ingest_timeout',
|
|
39
|
+
request: {
|
|
40
|
+
rid: 'R_ingest_timeout',
|
|
41
|
+
method: 'GET',
|
|
42
|
+
url: '/timeout',
|
|
43
|
+
status: 200,
|
|
44
|
+
durMs: 1,
|
|
45
|
+
},
|
|
46
|
+
t: Date.now(),
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
);
|
|
50
|
+
const elapsedMs = Date.now() - startedAt;
|
|
51
|
+
|
|
52
|
+
assert.equal(aborted, true, 'ingest fetch should be aborted on timeout');
|
|
53
|
+
assert(
|
|
54
|
+
elapsedMs < 500,
|
|
55
|
+
`ingest timeout should bound queue blocking; elapsed ${elapsedMs}ms`,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
console.log('ingest timeout OK');
|
|
59
|
+
} finally {
|
|
60
|
+
global.fetch = originalFetch;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
main().catch((error) => {
|
|
65
|
+
console.error(error);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
});
|
|
@@ -74,6 +74,10 @@ async function main() {
|
|
|
74
74
|
ingestBase: 'http://127.0.0.1:65535',
|
|
75
75
|
};
|
|
76
76
|
const { req, res } = makeTaggedReqRes('S_request_flush_timing', 'A_request_flush_timing');
|
|
77
|
+
let responseFinishedAt = 0;
|
|
78
|
+
res.on('finish', () => {
|
|
79
|
+
responseFinishedAt = Date.now();
|
|
80
|
+
});
|
|
77
81
|
|
|
78
82
|
await new Promise((resolve, reject) => {
|
|
79
83
|
void reproMiddleware(cfg)(req, res, async (err) => {
|
|
@@ -109,6 +113,19 @@ async function main() {
|
|
|
109
113
|
|
|
110
114
|
assert.strictEqual(countEvents(capturedBodies, 'backend_request'), 1, JSON.stringify(capturedBodies));
|
|
111
115
|
assert(countEvents(capturedBodies, 'trace_batch') >= 1, JSON.stringify(capturedBodies));
|
|
116
|
+
assert.strictEqual(
|
|
117
|
+
countEvents(capturedBodies, 'request_capture_manifest'),
|
|
118
|
+
1,
|
|
119
|
+
JSON.stringify(capturedBodies),
|
|
120
|
+
);
|
|
121
|
+
const manifestEvent = flattenEvents(capturedBodies)
|
|
122
|
+
.find((event) => event?.event_type === 'request_capture_manifest');
|
|
123
|
+
const manifestClosedAt = Number(manifestEvent?.payload?.requestCaptureManifest?.closedAt);
|
|
124
|
+
assert(Number.isFinite(manifestClosedAt), JSON.stringify(manifestEvent));
|
|
125
|
+
assert(
|
|
126
|
+
Math.abs(manifestClosedAt - responseFinishedAt) < 500,
|
|
127
|
+
`manifest closedAt should use response finish time; got ${manifestClosedAt}, finish ${responseFinishedAt}`,
|
|
128
|
+
);
|
|
112
129
|
|
|
113
130
|
console.log('request flush timing OK');
|
|
114
131
|
} finally {
|