@reproapp/node-sdk 0.0.16 → 0.0.17

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 CHANGED
@@ -1513,10 +1513,10 @@ function resolveCollectionOrWarn(source, type) {
1513
1513
  }
1514
1514
  return name;
1515
1515
  }
1516
- function post(cfg, sessionId, body) {
1516
+ function post(cfg, sessionId, body, options) {
1517
1517
  try {
1518
1518
  const entries = Array.isArray(body?.entries) ? body.entries : [];
1519
- (0, client_1.enqueueIngestEntries)(cfg, sessionId, entries);
1519
+ (0, client_1.enqueueIngestEntries)(cfg, sessionId, entries, options);
1520
1520
  }
1521
1521
  catch { /* swallow in SDK */ }
1522
1522
  }
@@ -3969,6 +3969,7 @@ function reproMiddleware(cfg) {
3969
3969
  let hardStopTimer = null;
3970
3970
  let flushPayload = null;
3971
3971
  let requestCaptureScheduled = false;
3972
+ let requestCaptureManifestScheduled = false;
3972
3973
  let sessionDrainWait = null;
3973
3974
  const activeSpans = new Set();
3974
3975
  let anonymousSpanDepth = 0;
@@ -4232,6 +4233,19 @@ function reproMiddleware(cfg) {
4232
4233
  // never break user code
4233
4234
  }
4234
4235
  };
4236
+ const emitRequestCaptureManifest = (traceBatchTotal) => {
4237
+ if (requestCaptureManifestScheduled)
4238
+ return;
4239
+ requestCaptureManifestScheduled = true;
4240
+ post(cfg, sid, {
4241
+ entries: [{
4242
+ actionId: aid,
4243
+ requestRid: rid,
4244
+ requestCaptureManifest: buildRequestCaptureManifestPayload(captureScope, traceBatchTotal, finishedAtAligned),
4245
+ t: finishedAtAligned ?? alignedNow(),
4246
+ }],
4247
+ }, { priority: 'high' });
4248
+ };
4235
4249
  try {
4236
4250
  if (__TRACER__?.tracer?.on) {
4237
4251
  const getTid = __TRACER__?.getCurrentTraceId;
@@ -4328,6 +4342,7 @@ function reproMiddleware(cfg) {
4328
4342
  void emitRequestCaptureAsync();
4329
4343
  if (!flushPayload) {
4330
4344
  flushPayload = async () => {
4345
+ let traceBatches = [];
4331
4346
  try {
4332
4347
  const pendingEvents = preparePendingTraceEventsForFlush(events.slice());
4333
4348
  const materializedEvents = await materializePendingTraceEventsAsync(pendingEvents, {
@@ -4340,38 +4355,39 @@ function reproMiddleware(cfg) {
4340
4355
  const orderedEvents = TRACE_ORDER_MODE === 'tree'
4341
4356
  ? reorderTraceEvents(baseEvents)
4342
4357
  : sortTraceEventsChronologically(baseEvents);
4343
- const traceBatches = chunkTraceEventsForTransport(orderedEvents, rid, aid);
4358
+ traceBatches = chunkTraceEventsForTransport(orderedEvents, rid, aid);
4344
4359
  if (traceBatches.length) {
4345
4360
  captureScope.traceBatchCount = traceBatches.length;
4346
- for (let i = 0; i < traceBatches.length; i++) {
4347
- const batch = traceBatches[i];
4348
- const traceValues = collectBatchTraceValueEntries(batch, i);
4349
- post(cfg, sid, {
4350
- entries: [{
4351
- actionId: aid,
4352
- trace: batch,
4353
- traceValues: traceValues.length ? traceValues : undefined,
4354
- traceBatch: {
4355
- rid,
4356
- index: i,
4357
- total: traceBatches.length,
4358
- },
4359
- t: alignedNow(),
4360
- }],
4361
- });
4362
- }
4363
4361
  }
4364
4362
  else {
4365
4363
  captureScope.traceBatchCount = 0;
4366
4364
  }
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
- });
4365
+ }
4366
+ catch {
4367
+ captureScope.traceBatchCount = 0;
4368
+ traceBatches = [];
4369
+ }
4370
+ finally {
4371
+ emitRequestCaptureManifest(traceBatches.length);
4372
+ }
4373
+ try {
4374
+ for (let i = 0; i < traceBatches.length; i++) {
4375
+ const batch = traceBatches[i];
4376
+ const traceValues = collectBatchTraceValueEntries(batch, i);
4377
+ post(cfg, sid, {
4378
+ entries: [{
4379
+ actionId: aid,
4380
+ trace: batch,
4381
+ traceValues: traceValues.length ? traceValues : undefined,
4382
+ traceBatch: {
4383
+ rid,
4384
+ index: i,
4385
+ total: traceBatches.length,
4386
+ },
4387
+ t: alignedNow(),
4388
+ }],
4389
+ });
4390
+ }
4375
4391
  }
4376
4392
  catch {
4377
4393
  // never break user code
@@ -1,10 +1,13 @@
1
1
  import type { IngestClientConfig, LegacyEntry } from './types';
2
+ export type EnqueueIngestOptions = {
3
+ priority?: 'normal' | 'high';
4
+ };
2
5
  export declare const configureIngestQueueDrain: (options: {
3
6
  shouldDefer?: (() => boolean) | null;
4
7
  deferMs?: number;
5
8
  }) => void;
6
9
  export declare const kickIngestQueueDrain: () => void;
7
10
  export declare const postIngestEntries: (config: IngestClientConfig, sessionId: string, entries: LegacyEntry[]) => Promise<void>;
8
- export declare const enqueueIngestEntries: (config: IngestClientConfig, sessionId: string, entries: LegacyEntry[]) => void;
11
+ export declare const enqueueIngestEntries: (config: IngestClientConfig, sessionId: string, entries: LegacyEntry[], options?: EnqueueIngestOptions) => void;
9
12
  export declare const flushIngestQueue: () => Promise<void>;
10
13
  export type { IngestClientConfig, LegacyEntry } from './types';
@@ -171,18 +171,41 @@ const drainIngestQueue = async () => {
171
171
  }
172
172
  }
173
173
  };
174
- const enqueueIngestEntries = (config, sessionId, entries) => {
174
+ const enqueueIngestEntries = (config, sessionId, entries, options = {}) => {
175
175
  if (!sessionId || !Array.isArray(entries) || entries.length === 0) {
176
176
  return;
177
177
  }
178
- ingestQueue.push({
178
+ const priority = options.priority === 'high' ? 'high' : 'normal';
179
+ const batch = {
179
180
  config: { ...config },
180
181
  sessionId,
181
182
  entries: entries.slice(),
182
183
  attempts: 0,
183
- });
184
+ priority,
185
+ };
186
+ if (priority === 'high') {
187
+ const firstNormalIndex = ingestQueue.findIndex((queued) => queued.priority !== 'high');
188
+ if (firstNormalIndex === -1) {
189
+ ingestQueue.push(batch);
190
+ }
191
+ else {
192
+ ingestQueue.splice(firstNormalIndex, 0, batch);
193
+ }
194
+ }
195
+ else {
196
+ ingestQueue.push(batch);
197
+ }
184
198
  if (DEFAULT_QUEUE_MAX_ITEMS > 0 && ingestQueue.length > DEFAULT_QUEUE_MAX_ITEMS) {
185
- ingestQueue.splice(0, ingestQueue.length - DEFAULT_QUEUE_MAX_ITEMS);
199
+ while (ingestQueue.length > DEFAULT_QUEUE_MAX_ITEMS) {
200
+ let removeIndex = ingestQueue.length - 1;
201
+ for (let index = ingestQueue.length - 1; index >= 0; index -= 1) {
202
+ if (ingestQueue[index]?.priority !== 'high') {
203
+ removeIndex = index;
204
+ break;
205
+ }
206
+ }
207
+ ingestQueue.splice(removeIndex, 1);
208
+ }
186
209
  }
187
210
  scheduleDrain(shouldDeferDrain?.() ? deferDrainMs : 0);
188
211
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reproapp/node-sdk",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
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 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"
15
+ "test": "npm run build && node test/unawaited.test.js && node test/integration-unawaited.js && node test/request-flush-timing.test.js && node test/request-manifest-priority.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
@@ -9,6 +9,7 @@ import {
9
9
  configureIngestQueueDrain,
10
10
  enqueueIngestEntries,
11
11
  kickIngestQueueDrain,
12
+ type EnqueueIngestOptions,
12
13
  type IngestClientConfig,
13
14
  type LegacyEntry,
14
15
  } from './ingest/client';
@@ -1908,10 +1909,11 @@ function post(
1908
1909
  cfg: IngestClientConfig,
1909
1910
  sessionId: string,
1910
1911
  body: any,
1912
+ options?: EnqueueIngestOptions,
1911
1913
  ) {
1912
1914
  try {
1913
1915
  const entries = Array.isArray(body?.entries) ? (body.entries as LegacyEntry[]) : [];
1914
- enqueueIngestEntries(cfg, sessionId, entries);
1916
+ enqueueIngestEntries(cfg, sessionId, entries, options);
1915
1917
  } catch { /* swallow in SDK */ }
1916
1918
  }
1917
1919
 
@@ -4856,6 +4858,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
4856
4858
  let hardStopTimer: NodeJS.Timeout | null = null;
4857
4859
  let flushPayload: null | (() => Promise<void>) = null;
4858
4860
  let requestCaptureScheduled = false;
4861
+ let requestCaptureManifestScheduled = false;
4859
4862
  let sessionDrainWait: Promise<void> | null = null;
4860
4863
  const activeSpans = new Set<string>();
4861
4864
  let anonymousSpanDepth = 0;
@@ -5150,6 +5153,23 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5150
5153
  }
5151
5154
  };
5152
5155
 
5156
+ const emitRequestCaptureManifest = (traceBatchTotal: number): void => {
5157
+ if (requestCaptureManifestScheduled) return;
5158
+ requestCaptureManifestScheduled = true;
5159
+ post(cfg, sid, {
5160
+ entries: [{
5161
+ actionId: aid,
5162
+ requestRid: rid,
5163
+ requestCaptureManifest: buildRequestCaptureManifestPayload(
5164
+ captureScope,
5165
+ traceBatchTotal,
5166
+ finishedAtAligned,
5167
+ ),
5168
+ t: finishedAtAligned ?? alignedNow(),
5169
+ }],
5170
+ }, { priority: 'high' });
5171
+ };
5172
+
5153
5173
  try {
5154
5174
  if (__TRACER__?.tracer?.on) {
5155
5175
  const getTid = __TRACER__?.getCurrentTraceId;
@@ -5250,6 +5270,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5250
5270
 
5251
5271
  if (!flushPayload) {
5252
5272
  flushPayload = async () => {
5273
+ let traceBatches: TraceEventRecord[][] = [];
5253
5274
  try {
5254
5275
  const pendingEvents = preparePendingTraceEventsForFlush(events.slice());
5255
5276
  const materializedEvents = await materializePendingTraceEventsAsync(pendingEvents, {
@@ -5262,7 +5283,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5262
5283
  const orderedEvents = TRACE_ORDER_MODE === 'tree'
5263
5284
  ? reorderTraceEvents(baseEvents)
5264
5285
  : sortTraceEventsChronologically(baseEvents);
5265
- const traceBatches = chunkTraceEventsForTransport(
5286
+ traceBatches = chunkTraceEventsForTransport(
5266
5287
  orderedEvents,
5267
5288
  rid,
5268
5289
  aid,
@@ -5270,40 +5291,36 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5270
5291
 
5271
5292
  if (traceBatches.length) {
5272
5293
  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
- });
5290
- }
5291
5294
  } else {
5292
5295
  captureScope.traceBatchCount = 0;
5293
5296
  }
5294
5297
 
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
- });
5298
+ } catch {
5299
+ captureScope.traceBatchCount = 0;
5300
+ traceBatches = [];
5301
+ } finally {
5302
+ emitRequestCaptureManifest(traceBatches.length);
5303
+ }
5304
+
5305
+ try {
5306
+ for (let i = 0; i < traceBatches.length; i++) {
5307
+ const batch = traceBatches[i];
5308
+ const traceValues = collectBatchTraceValueEntries(batch, i);
5309
+
5310
+ post(cfg, sid, {
5311
+ entries: [{
5312
+ actionId: aid,
5313
+ trace: batch,
5314
+ traceValues: traceValues.length ? traceValues : undefined,
5315
+ traceBatch: {
5316
+ rid,
5317
+ index: i,
5318
+ total: traceBatches.length,
5319
+ },
5320
+ t: alignedNow(),
5321
+ }],
5322
+ });
5323
+ }
5307
5324
  } catch {
5308
5325
  // never break user code
5309
5326
  }
@@ -26,6 +26,11 @@ type QueuedIngestBatch = {
26
26
  sessionId: string;
27
27
  entries: LegacyEntry[];
28
28
  attempts: number;
29
+ priority: 'normal' | 'high';
30
+ };
31
+
32
+ export type EnqueueIngestOptions = {
33
+ priority?: 'normal' | 'high';
29
34
  };
30
35
 
31
36
  const ingestQueue: QueuedIngestBatch[] = [];
@@ -205,18 +210,40 @@ export const enqueueIngestEntries = (
205
210
  config: IngestClientConfig,
206
211
  sessionId: string,
207
212
  entries: LegacyEntry[],
213
+ options: EnqueueIngestOptions = {},
208
214
  ): void => {
209
215
  if (!sessionId || !Array.isArray(entries) || entries.length === 0) {
210
216
  return;
211
217
  }
212
- ingestQueue.push({
218
+ const priority = options.priority === 'high' ? 'high' : 'normal';
219
+ const batch: QueuedIngestBatch = {
213
220
  config: { ...config },
214
221
  sessionId,
215
222
  entries: entries.slice(),
216
223
  attempts: 0,
217
- });
224
+ priority,
225
+ };
226
+ if (priority === 'high') {
227
+ const firstNormalIndex = ingestQueue.findIndex((queued) => queued.priority !== 'high');
228
+ if (firstNormalIndex === -1) {
229
+ ingestQueue.push(batch);
230
+ } else {
231
+ ingestQueue.splice(firstNormalIndex, 0, batch);
232
+ }
233
+ } else {
234
+ ingestQueue.push(batch);
235
+ }
218
236
  if (DEFAULT_QUEUE_MAX_ITEMS > 0 && ingestQueue.length > DEFAULT_QUEUE_MAX_ITEMS) {
219
- ingestQueue.splice(0, ingestQueue.length - DEFAULT_QUEUE_MAX_ITEMS);
237
+ while (ingestQueue.length > DEFAULT_QUEUE_MAX_ITEMS) {
238
+ let removeIndex = ingestQueue.length - 1;
239
+ for (let index = ingestQueue.length - 1; index >= 0; index -= 1) {
240
+ if (ingestQueue[index]?.priority !== 'high') {
241
+ removeIndex = index;
242
+ break;
243
+ }
244
+ }
245
+ ingestQueue.splice(removeIndex, 1);
246
+ }
220
247
  }
221
248
  scheduleDrain(shouldDeferDrain?.() ? deferDrainMs : 0);
222
249
  };
@@ -0,0 +1,147 @@
1
+ process.env.REPRO_INGEST_REQUEST_TIMEOUT_MS = '50';
2
+ process.env.REPRO_SDK_BACKGROUND_MAX_DEFER_MS = '25';
3
+ process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '0';
4
+ process.env.REPRO_SDK_BACKGROUND_BUSY_IDLE_DELAY_MS = '0';
5
+ process.env.TRACE_IDLE_FLUSH_MS = '10';
6
+ process.env.TRACE_LINGER_AFTER_FINISH_MS = '0';
7
+
8
+ const assert = require('assert');
9
+ const { EventEmitter } = require('events');
10
+ const { initReproTracing, reproMiddleware } = require('../dist');
11
+ const { flushIngestQueue } = require('../dist/ingest/client');
12
+
13
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
14
+
15
+ async function waitFor(predicate, timeoutMs = 1500) {
16
+ const deadline = Date.now() + timeoutMs;
17
+ while (Date.now() < deadline) {
18
+ if (predicate()) return;
19
+ await sleep(20);
20
+ }
21
+ assert(predicate(), 'timed out waiting for expected condition');
22
+ }
23
+
24
+ function makeTaggedReqRes(sessionId, actionId) {
25
+ const req = new EventEmitter();
26
+ req.method = 'GET';
27
+ req.url = '/manifest-priority';
28
+ req.headers = {
29
+ 'x-bug-session-id': sessionId,
30
+ 'x-bug-action-id': actionId,
31
+ 'x-bug-request-start': String(Date.now()),
32
+ };
33
+ req.body = undefined;
34
+ req.params = {};
35
+ req.query = {};
36
+
37
+ const res = new EventEmitter();
38
+ res.statusCode = 200;
39
+ res.getHeader = () => undefined;
40
+ res.setHeader = () => {};
41
+ res.json = function (body) { this.body = body; this.emit('finish'); return body; };
42
+ res.send = function (body) { this.body = body; this.emit('finish'); return body; };
43
+ res.write = () => true;
44
+ res.end = () => { res.emit('finish'); return true; };
45
+
46
+ return { req, res };
47
+ }
48
+
49
+ function flattenEvents(posts) {
50
+ return posts.flatMap((body) => Array.isArray(body?.events) ? body.events : []);
51
+ }
52
+
53
+ async function tracedHandler() {
54
+ return {
55
+ ok: true,
56
+ payload: 'x'.repeat(16_000),
57
+ };
58
+ }
59
+
60
+ async function main() {
61
+ const capturedBodies = [];
62
+ const originalFetch = global.fetch;
63
+ global.fetch = async (_url, init = {}) => {
64
+ const body = JSON.parse(String(init.body || '{}'));
65
+ capturedBodies.push(body);
66
+ const hasTraceBatch = Array.isArray(body.events) &&
67
+ body.events.some((event) => event?.event_type === 'trace_batch');
68
+ if (hasTraceBatch) {
69
+ return new Promise((_resolve, reject) => {
70
+ const signal = init.signal;
71
+ if (signal?.aborted) {
72
+ reject(new Error('aborted'));
73
+ return;
74
+ }
75
+ signal?.addEventListener?.('abort', () => reject(new Error('aborted')), { once: true });
76
+ });
77
+ }
78
+ return {
79
+ ok: true,
80
+ status: 202,
81
+ json: async () => ({ ok: true }),
82
+ text: async () => '{"ok":true}',
83
+ };
84
+ };
85
+
86
+ try {
87
+ initReproTracing({ instrument: false, logFunctionCalls: false });
88
+ const cfg = {
89
+ tenantId: 'TENANT_test',
90
+ appId: 'APP_test',
91
+ appSecret: 'secret',
92
+ captureHeaders: false,
93
+ privacy: { environment: 'dev' },
94
+ ingestBase: 'http://127.0.0.1:65535',
95
+ };
96
+ const { req, res } = makeTaggedReqRes('S_manifest_priority', 'A_manifest_priority');
97
+
98
+ await new Promise((resolve, reject) => {
99
+ void reproMiddleware(cfg)(req, res, async (err) => {
100
+ if (err) {
101
+ reject(err);
102
+ return;
103
+ }
104
+
105
+ try {
106
+ await global.__repro_call(
107
+ tracedHandler,
108
+ null,
109
+ [],
110
+ 'app',
111
+ 1,
112
+ 'tracedHandler',
113
+ true,
114
+ );
115
+ res.json({ ok: true });
116
+ resolve();
117
+ } catch (error) {
118
+ reject(error);
119
+ }
120
+ });
121
+ });
122
+
123
+ await waitFor(() => flattenEvents(capturedBodies)
124
+ .some((event) => event?.event_type === 'request_capture_manifest'));
125
+
126
+ const eventTypes = flattenEvents(capturedBodies).map((event) => event?.event_type);
127
+ const manifestIndex = eventTypes.indexOf('request_capture_manifest');
128
+ const traceIndex = eventTypes.indexOf('trace_batch');
129
+
130
+ assert(manifestIndex >= 0, JSON.stringify(eventTypes));
131
+ assert(traceIndex >= 0, JSON.stringify(eventTypes));
132
+ assert(
133
+ manifestIndex < traceIndex,
134
+ `request_capture_manifest must be sent before trace_batch; got ${JSON.stringify(eventTypes)}`,
135
+ );
136
+
137
+ console.log('request manifest priority OK');
138
+ } finally {
139
+ global.fetch = originalFetch;
140
+ await flushIngestQueue();
141
+ }
142
+ }
143
+
144
+ main().catch((error) => {
145
+ console.error(error);
146
+ process.exitCode = 1;
147
+ });