@reproapp/node-sdk 0.0.20 → 0.0.21

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
@@ -4175,13 +4175,16 @@ function reproMiddleware(cfg) {
4175
4175
  privacy: activePrivacy,
4176
4176
  },
4177
4177
  }, requestValueEntries);
4178
+ const responseDurationMs = finishedAt !== null && Number.isFinite(finishedAt)
4179
+ ? Math.max(0, finishedAt - t0)
4180
+ : Date.now() - t0;
4178
4181
  const requestPayload = {
4179
4182
  rid,
4180
4183
  method: req.method,
4181
4184
  url,
4182
4185
  path,
4183
4186
  status: res.statusCode,
4184
- durMs: Date.now() - t0,
4187
+ durMs: responseDurationMs,
4185
4188
  headers: maskedHeaders,
4186
4189
  key,
4187
4190
  respBody: responseBody,
@@ -4217,6 +4220,12 @@ function reproMiddleware(cfg) {
4217
4220
  return;
4218
4221
  requestCaptureScheduled = true;
4219
4222
  try {
4223
+ if (capturedBody === undefined && chunks.length) {
4224
+ const buf = Buffer.isBuffer(chunks[0])
4225
+ ? Buffer.concat(chunks.map(c => (Buffer.isBuffer(c) ? c : Buffer.from(String(c)))))
4226
+ : Buffer.from(chunks.map(String).join(''));
4227
+ capturedBody = coerceBodyToStorable(buf, res.getHeader?.('content-type'));
4228
+ }
4220
4229
  const { chosenEndpoint, hasTraceEvents } = chooseRequestEndpoint();
4221
4230
  const { requestPayload, requestValueEntries } = await buildRequestCapturePayloadAsync(chosenEndpoint, hasTraceEvents);
4222
4231
  post(cfg, sid, {
@@ -4333,13 +4342,7 @@ function reproMiddleware(cfg) {
4333
4342
  finishedAt = Date.now();
4334
4343
  finishedAtAligned = alignTimestamp(finishedAt);
4335
4344
  }
4336
- if (capturedBody === undefined && chunks.length) {
4337
- const buf = Buffer.isBuffer(chunks[0])
4338
- ? Buffer.concat(chunks.map(c => (Buffer.isBuffer(c) ? c : Buffer.from(String(c)))))
4339
- : Buffer.from(chunks.map(String).join(''));
4340
- capturedBody = coerceBodyToStorable(buf, res.getHeader?.('content-type'));
4341
- }
4342
- void emitRequestCaptureAsync();
4345
+ scheduleSdkBackgroundWork(emitRequestCaptureAsync, { priority: 'high' });
4343
4346
  if (!flushPayload) {
4344
4347
  flushPayload = async () => {
4345
4348
  let traceBatches = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reproapp/node-sdk",
3
- "version": "0.0.20",
3
+ "version": "0.0.21",
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/request-manifest-priority.test.js && node test/runtime-no-double-invoke.test.js && node test/express-trace-http-args.test.js && node test/trace-batch-size.test.js && node test/sdk-background-priority.test.js && node test/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/wrap-plugin-super-call.test.js && node test/function-arg-identity.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-capture-background.test.js && node test/request-manifest-priority.test.js && node test/runtime-no-double-invoke.test.js && node test/express-trace-http-args.test.js && node test/trace-batch-size.test.js && node test/sdk-background-priority.test.js && node test/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/wrap-plugin-super-call.test.js && node test/function-arg-identity.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
  "test:containers": "npm run build && node test/runtime-no-double-invoke.testcontainers.test.js && node test/wrap-plugin-super-call.testcontainers.test.js && node test/function-arg-identity.testcontainers.test.js"
17
17
  },
18
18
  "peerDependencies": {
package/src/index.ts CHANGED
@@ -5099,13 +5099,17 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5099
5099
  },
5100
5100
  }, requestValueEntries);
5101
5101
 
5102
+ const responseDurationMs =
5103
+ finishedAt !== null && Number.isFinite(finishedAt)
5104
+ ? Math.max(0, finishedAt - t0)
5105
+ : Date.now() - t0;
5102
5106
  const requestPayload: Record<string, any> = {
5103
5107
  rid,
5104
5108
  method: req.method,
5105
5109
  url,
5106
5110
  path,
5107
5111
  status: res.statusCode,
5108
- durMs: Date.now() - t0,
5112
+ durMs: responseDurationMs,
5109
5113
  headers: maskedHeaders,
5110
5114
  key,
5111
5115
  respBody: responseBody,
@@ -5134,6 +5138,13 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5134
5138
  if (requestCaptureScheduled) return;
5135
5139
  requestCaptureScheduled = true;
5136
5140
  try {
5141
+ if (capturedBody === undefined && chunks.length) {
5142
+ const buf = Buffer.isBuffer(chunks[0])
5143
+ ? Buffer.concat(chunks.map(c => (Buffer.isBuffer(c) ? c : Buffer.from(String(c)))))
5144
+ : Buffer.from(chunks.map(String).join(''));
5145
+ capturedBody = coerceBodyToStorable(buf, res.getHeader?.('content-type'));
5146
+ }
5147
+
5137
5148
  const { chosenEndpoint, hasTraceEvents } = chooseRequestEndpoint();
5138
5149
  const { requestPayload, requestValueEntries } = await buildRequestCapturePayloadAsync(
5139
5150
  chosenEndpoint,
@@ -5259,14 +5270,8 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5259
5270
  finishedAt = Date.now();
5260
5271
  finishedAtAligned = alignTimestamp(finishedAt);
5261
5272
  }
5262
- if (capturedBody === undefined && chunks.length) {
5263
- const buf = Buffer.isBuffer(chunks[0])
5264
- ? Buffer.concat(chunks.map(c => (Buffer.isBuffer(c) ? c : Buffer.from(String(c)))))
5265
- : Buffer.from(chunks.map(String).join(''));
5266
- capturedBody = coerceBodyToStorable(buf, res.getHeader?.('content-type'));
5267
- }
5268
5273
 
5269
- void emitRequestCaptureAsync();
5274
+ scheduleSdkBackgroundWork(emitRequestCaptureAsync, { priority: 'high' });
5270
5275
 
5271
5276
  if (!flushPayload) {
5272
5277
  flushPayload = async () => {
@@ -0,0 +1,131 @@
1
+ process.env.REPRO_SDK_BACKGROUND_MAX_DEFER_MS = '500';
2
+ process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '25';
3
+ process.env.REPRO_SDK_BACKGROUND_BUSY_IDLE_DELAY_MS = '0';
4
+ process.env.TRACE_IDLE_FLUSH_MS = '10';
5
+ process.env.TRACE_LINGER_AFTER_FINISH_MS = '0';
6
+
7
+ const assert = require('node:assert/strict');
8
+ const { EventEmitter } = require('events');
9
+ const { reproMiddleware } = require('../dist');
10
+ const { flushIngestQueue } = require('../dist/ingest/client');
11
+
12
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
13
+
14
+ async function waitFor(predicate, timeoutMs = 1500) {
15
+ const deadline = Date.now() + timeoutMs;
16
+ while (Date.now() < deadline) {
17
+ if (predicate()) return;
18
+ await sleep(20);
19
+ }
20
+ assert(predicate(), 'timed out waiting for expected condition');
21
+ }
22
+
23
+ function makeReqRes(sessionId, actionId) {
24
+ const req = new EventEmitter();
25
+ req.method = 'GET';
26
+ req.url = sessionId ? '/captured' : '/held-open';
27
+ req.headers = sessionId && actionId
28
+ ? {
29
+ 'x-bug-session-id': sessionId,
30
+ 'x-bug-action-id': actionId,
31
+ 'x-bug-request-start': String(Date.now()),
32
+ }
33
+ : {};
34
+ req.body = undefined;
35
+ req.params = {};
36
+ req.query = {};
37
+
38
+ const res = new EventEmitter();
39
+ res.statusCode = 200;
40
+ res.getHeader = () => 'application/json';
41
+ res.setHeader = () => {};
42
+ res.json = function (body) { this.body = body; this.emit('finish'); return body; };
43
+ res.send = function (body) { this.body = body; this.emit('finish'); return body; };
44
+ res.write = () => true;
45
+ res.end = () => { res.emit('finish'); return true; };
46
+
47
+ return { req, res };
48
+ }
49
+
50
+ function flattenEvents(posts) {
51
+ return posts.flatMap((body) => Array.isArray(body?.events) ? body.events : []);
52
+ }
53
+
54
+ function countEvents(posts, eventType) {
55
+ return flattenEvents(posts).filter((event) => event?.event_type === eventType).length;
56
+ }
57
+
58
+ async function invokeMiddleware(middleware, req, res, handler) {
59
+ await new Promise((resolve, reject) => {
60
+ void middleware(req, res, async (err) => {
61
+ if (err) {
62
+ reject(err);
63
+ return;
64
+ }
65
+ try {
66
+ await handler();
67
+ resolve();
68
+ } catch (error) {
69
+ reject(error);
70
+ }
71
+ });
72
+ });
73
+ }
74
+
75
+ async function main() {
76
+ const capturedBodies = [];
77
+ const originalFetch = global.fetch;
78
+ global.fetch = async (_url, init = {}) => {
79
+ capturedBodies.push(JSON.parse(String(init.body || '{}')));
80
+ return {
81
+ ok: true,
82
+ status: 202,
83
+ json: async () => ({ ok: true }),
84
+ text: async () => '{"ok":true}',
85
+ };
86
+ };
87
+
88
+ try {
89
+ const cfg = {
90
+ tenantId: 'TENANT_test',
91
+ appId: 'APP_test',
92
+ appSecret: 'secret',
93
+ captureHeaders: false,
94
+ privacy: { environment: 'dev' },
95
+ ingestBase: 'http://127.0.0.1:65535',
96
+ };
97
+ const middleware = reproMiddleware(cfg);
98
+
99
+ const held = makeReqRes(null, null);
100
+ await invokeMiddleware(middleware, held.req, held.res, async () => {
101
+ // Keep this request open to simulate another client endpoint still in flight.
102
+ });
103
+
104
+ const tagged = makeReqRes('S_request_capture_background', 'A_request_capture_background');
105
+ await invokeMiddleware(middleware, tagged.req, tagged.res, async () => {
106
+ tagged.res.json({ ok: true, rows: Array.from({ length: 50 }, (_, i) => ({ i, value: `row-${i}` })) });
107
+ });
108
+
109
+ await sleep(100);
110
+ assert.equal(
111
+ countEvents(capturedBodies, 'backend_request'),
112
+ 0,
113
+ `request capture should defer while another client request is active: ${JSON.stringify(capturedBodies)}`,
114
+ );
115
+
116
+ held.res.emit('finish');
117
+ await waitFor(() => countEvents(capturedBodies, 'backend_request') === 1);
118
+ await flushIngestQueue();
119
+
120
+ assert.equal(countEvents(capturedBodies, 'backend_request'), 1, JSON.stringify(capturedBodies));
121
+ console.log('request capture background OK');
122
+ } finally {
123
+ global.fetch = originalFetch;
124
+ await flushIngestQueue();
125
+ }
126
+ }
127
+
128
+ main().catch((error) => {
129
+ console.error(error);
130
+ process.exit(1);
131
+ });