@reproapp/node-sdk 0.0.2 → 0.0.4

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.
Files changed (45) hide show
  1. package/README.md +36 -2
  2. package/dist/index.d.ts +140 -15
  3. package/dist/index.js +4927 -927
  4. package/dist/ingest/client.d.ts +10 -0
  5. package/dist/ingest/client.js +158 -0
  6. package/dist/ingest/mapper.d.ts +2 -0
  7. package/dist/ingest/mapper.js +92 -0
  8. package/dist/ingest/types.d.ts +40 -0
  9. package/dist/ingest/types.js +2 -0
  10. package/dist/ingest/worker.js +19 -0
  11. package/dist/integrations/sendgrid.d.ts +2 -4
  12. package/dist/integrations/sendgrid.js +4 -14
  13. package/dist/privacy-fallback.d.ts +1 -0
  14. package/dist/privacy-fallback.js +27 -0
  15. package/dist/privacy-redaction.d.ts +3 -0
  16. package/dist/privacy-redaction.js +38 -0
  17. package/dist/privacy.d.ts +108 -0
  18. package/dist/privacy.js +2868 -0
  19. package/dist/trace-materializer-worker.d.ts +1 -0
  20. package/dist/trace-materializer-worker.js +33 -0
  21. package/docs/tracing.md +1 -0
  22. package/package.json +9 -3
  23. package/src/index.ts +5583 -954
  24. package/src/ingest/client.ts +194 -0
  25. package/src/ingest/mapper.ts +104 -0
  26. package/src/ingest/types.ts +42 -0
  27. package/src/integrations/sendgrid.ts +6 -19
  28. package/src/privacy-fallback.ts +25 -0
  29. package/src/privacy-redaction.ts +37 -0
  30. package/src/privacy.ts +3593 -0
  31. package/src/trace-materializer-worker.ts +39 -0
  32. package/test/circular-capture.test.js +111 -0
  33. package/test/disable-subtree.test.js +154 -0
  34. package/test/integration-unawaited.js +183 -0
  35. package/test/kafka-runtime-privacy-policy.test.js +285 -0
  36. package/test/privacy-runtime-policy.test.js +2043 -0
  37. package/test/promise-map.test.js +72 -0
  38. package/test/unawaited.test.js +163 -0
  39. package/test/wrap-plugin-arrow-args.test.js +80 -0
  40. package/tracer/cjs-hook.js +0 -1
  41. package/tracer/wrap-plugin.js +96 -10
  42. package/dist/redaction.d.ts +0 -44
  43. package/dist/redaction.js +0 -167
  44. package/dist/server.js +0 -26
  45. /package/dist/{server.d.ts → ingest/worker.d.ts} +0 -0
@@ -0,0 +1,39 @@
1
+ import { parentPort } from 'node:worker_threads';
2
+ import { __materializePendingTraceEventsForWorker } from './index';
3
+
4
+ type TraceMaterializationWorkerRequest = {
5
+ taskId: number;
6
+ type: 'materialize-trace-events';
7
+ payload: any;
8
+ };
9
+
10
+ function isTraceMaterializationWorkerRequest(value: unknown): value is TraceMaterializationWorkerRequest {
11
+ if (!value || typeof value !== 'object') {
12
+ return false;
13
+ }
14
+ const message = value as TraceMaterializationWorkerRequest;
15
+ return Number.isFinite(message.taskId) && message.type === 'materialize-trace-events' && !!message.payload;
16
+ }
17
+
18
+ parentPort?.on('message', (message: unknown) => {
19
+ if (!isTraceMaterializationWorkerRequest(message)) {
20
+ return;
21
+ }
22
+
23
+ void (async () => {
24
+ try {
25
+ const events = await __materializePendingTraceEventsForWorker(message.payload);
26
+ parentPort?.postMessage({
27
+ taskId: message.taskId,
28
+ ok: true,
29
+ events,
30
+ });
31
+ } catch (err) {
32
+ parentPort?.postMessage({
33
+ taskId: message.taskId,
34
+ ok: false,
35
+ error: err instanceof Error ? err.message : String(err),
36
+ });
37
+ }
38
+ })();
39
+ });
@@ -0,0 +1,111 @@
1
+ process.env.TRACE_LINGER_AFTER_FINISH_MS = '0';
2
+ process.env.TRACE_IDLE_FLUSH_MS = '25';
3
+ process.env.SESSION_DRAIN_TIMEOUT_MS = '0';
4
+
5
+ const assert = require('assert');
6
+ const { EventEmitter } = require('events');
7
+ const { initReproTracing, reproMiddleware } = require('../dist');
8
+
9
+ function makeReqRes() {
10
+ const req = new EventEmitter();
11
+ req.method = 'POST';
12
+ req.url = '/circular';
13
+ req.headers = {
14
+ 'content-type': 'application/json',
15
+ 'x-bug-session-id': 'sid-circular',
16
+ 'x-bug-action-id': 'aid-circular',
17
+ 'x-bug-request-start': String(Date.now()),
18
+ };
19
+ req.body = { ok: true };
20
+ req.params = {};
21
+ req.query = {};
22
+
23
+ const res = new EventEmitter();
24
+ res.statusCode = 200;
25
+ res.getHeader = () => undefined;
26
+ res.setHeader = () => {};
27
+ res.json = function (body) { this.body = body; this.emit('finish'); return body; };
28
+ res.send = function (body) { this.body = body; this.emit('finish'); return body; };
29
+ res.write = () => true;
30
+ res.end = () => { res.emit('finish'); return true; };
31
+
32
+ return { req, res };
33
+ }
34
+
35
+ function readTraceArray(value) {
36
+ if (Array.isArray(value)) return value;
37
+ if (typeof value !== 'string') return [];
38
+ try {
39
+ const parsed = JSON.parse(value);
40
+ return Array.isArray(parsed) ? parsed : [];
41
+ } catch {
42
+ return [];
43
+ }
44
+ }
45
+
46
+ function collectTraceEvents(posts) {
47
+ const events = [];
48
+ for (const post of posts) {
49
+ let payload;
50
+ try { payload = JSON.parse(String(post.body || '')); } catch { continue; }
51
+ for (const entry of Array.isArray(payload?.entries) ? payload.entries : []) {
52
+ events.push(...readTraceArray(entry?.trace));
53
+ events.push(...readTraceArray(entry?.request?.trace));
54
+ }
55
+ for (const event of Array.isArray(payload?.events) ? payload.events : []) {
56
+ events.push(...readTraceArray(event?.payload?.trace));
57
+ events.push(...readTraceArray(event?.payload?.request?.trace));
58
+ }
59
+ }
60
+ return events;
61
+ }
62
+
63
+ async function circularFn() {
64
+ const subject = { name: 'Avery Debugson' };
65
+ const root = { subject, copy: subject };
66
+ subject.parent = root;
67
+ return root;
68
+ }
69
+
70
+ async function main() {
71
+ const posts = [];
72
+ global.fetch = async (url, opts = {}) => {
73
+ posts.push({ url: String(url), body: opts.body });
74
+ return { ok: true, status: 200, json: async () => ({ ok: true }) };
75
+ };
76
+
77
+ initReproTracing({ instrument: false, logFunctionCalls: false });
78
+ const middleware = reproMiddleware({
79
+ appId: 'app',
80
+ tenantId: 'tenant',
81
+ appSecret: 'secret',
82
+ captureHeaders: false,
83
+ });
84
+ const { req, res } = makeReqRes();
85
+
86
+ await new Promise((resolve, reject) => {
87
+ middleware(req, res, async () => {
88
+ try {
89
+ await global.__repro_call(circularFn, null, [], 'app', 1, 'circularFn', false);
90
+ res.json({ ok: true });
91
+ resolve();
92
+ } catch (err) {
93
+ reject(err);
94
+ }
95
+ });
96
+ });
97
+ await new Promise((resolve) => setTimeout(resolve, 120));
98
+
99
+ const exitEvent = collectTraceEvents(posts).find((event) => event?.fn === 'circularFn' && event?.type === 'exit');
100
+ assert(exitEvent, 'expected circularFn exit trace');
101
+ assert.strictEqual(exitEvent.returnValue.copy.__reproCircularRef, '$.subject');
102
+ assert.strictEqual(exitEvent.returnValue.subject.parent.__reproCircularRef, '$');
103
+ assert(!JSON.stringify(exitEvent).includes('[Circular]'), 'expected path references instead of opaque [Circular] strings');
104
+
105
+ console.log('circular capture references OK');
106
+ }
107
+
108
+ main().catch((err) => {
109
+ console.error(err);
110
+ process.exitCode = 1;
111
+ });
@@ -0,0 +1,154 @@
1
+ // Integration-style test: disabling a function trace should also suppress its descendants.
2
+ process.env.TRACE_LINGER_AFTER_FINISH_MS = '0';
3
+ process.env.TRACE_IDLE_FLUSH_MS = '25';
4
+ process.env.SESSION_DRAIN_TIMEOUT_MS = '0';
5
+
6
+ const assert = require('assert');
7
+ const { EventEmitter } = require('events');
8
+ const { initReproTracing, reproMiddleware, setDisabledFunctionTraces } = require('../dist');
9
+
10
+ global.fetch = async () => ({ ok: true, status: 200, json: async () => ({ ok: true }) });
11
+
12
+ const childFn = async function childFn() {
13
+ return { ok: true };
14
+ };
15
+
16
+ const parentFn = async function parentFn() {
17
+ // Trace the child call so we can verify subtree suppression.
18
+ await global.__repro_call(childFn, null, [], 'app', 2, 'childFn', false);
19
+ return { ok: true };
20
+ };
21
+
22
+ function makeReqRes(sessionId, actionId) {
23
+ const req = new EventEmitter();
24
+ req.method = 'POST';
25
+ req.url = '/test';
26
+ req.headers = {
27
+ 'content-type': 'application/json',
28
+ 'x-bug-session-id': sessionId,
29
+ 'x-bug-action-id': actionId,
30
+ 'x-bug-request-start': String(Date.now()),
31
+ };
32
+ req.body = { ok: true };
33
+ req.params = {};
34
+ req.query = {};
35
+
36
+ const res = new EventEmitter();
37
+ res.statusCode = 200;
38
+ res.getHeader = () => undefined;
39
+ res.setHeader = () => {};
40
+ res.json = function (body) { this.body = body; this.emit('finish'); return body; };
41
+ res.send = function (body) { this.body = body; this.emit('finish'); return body; };
42
+ res.write = () => true;
43
+ res.end = () => { res.emit('finish'); return true; };
44
+
45
+ return { req, res };
46
+ }
47
+
48
+ function collectTraceFnsFromPosts(posts) {
49
+ const readTraceArray = (value) => {
50
+ if (Array.isArray(value)) return value;
51
+ if (typeof value !== 'string') return null;
52
+ try {
53
+ const parsed = JSON.parse(value);
54
+ return Array.isArray(parsed) ? parsed : null;
55
+ } catch {
56
+ return null;
57
+ }
58
+ };
59
+
60
+ const fns = [];
61
+ for (const post of posts) {
62
+ let payload;
63
+ try { payload = JSON.parse(String(post.body || '')); } catch { continue; }
64
+ const entries = Array.isArray(payload?.entries) ? payload.entries : [];
65
+ for (const entry of entries) {
66
+ const entryTrace = readTraceArray(entry?.trace);
67
+ if (entryTrace) {
68
+ entryTrace.forEach(ev => { if (ev && ev.fn) fns.push(String(ev.fn)); });
69
+ }
70
+
71
+ const requestTrace = readTraceArray(entry?.request?.trace);
72
+ if (requestTrace) {
73
+ requestTrace.forEach(ev => { if (ev && ev.fn) fns.push(String(ev.fn)); });
74
+ }
75
+ }
76
+
77
+ const events = Array.isArray(payload?.events) ? payload.events : [];
78
+ for (const event of events) {
79
+ const payloadTrace = readTraceArray(event?.payload?.trace);
80
+ if (payloadTrace) {
81
+ payloadTrace.forEach(ev => { if (ev && ev.fn) fns.push(String(ev.fn)); });
82
+ }
83
+
84
+ const payloadRequestTrace = readTraceArray(event?.payload?.request?.trace);
85
+ if (payloadRequestTrace) {
86
+ payloadRequestTrace.forEach(ev => { if (ev && ev.fn) fns.push(String(ev.fn)); });
87
+ }
88
+ }
89
+ }
90
+ return fns;
91
+ }
92
+
93
+ async function runScenario(disableRules) {
94
+ const posts = [];
95
+ global.fetch = async (url, opts = {}) => {
96
+ posts.push({ url: String(url), body: opts.body });
97
+ return { ok: true, status: 200, json: async () => ({ ok: true }) };
98
+ };
99
+
100
+ setDisabledFunctionTraces(disableRules ?? null);
101
+
102
+ const middleware = reproMiddleware({
103
+ appId: 'app',
104
+ tenantId: 'tenant',
105
+ appSecret: 'secret',
106
+ captureHeaders: false,
107
+ });
108
+
109
+ const { req, res } = makeReqRes('sid', disableRules ? 'aid-disabled' : 'aid-enabled');
110
+
111
+ await new Promise((resolve, reject) => {
112
+ middleware(req, res, async () => {
113
+ try {
114
+ await global.__repro_call(parentFn, null, [], 'app', 1, 'parentFn', false);
115
+ res.json({ ok: true });
116
+ resolve();
117
+ } catch (err) {
118
+ reject(err);
119
+ }
120
+ });
121
+ });
122
+
123
+ // Allow flush timers to send payloads.
124
+ await new Promise(r => setTimeout(r, 120));
125
+
126
+ return collectTraceFnsFromPosts(posts);
127
+ }
128
+
129
+ async function main() {
130
+ initReproTracing({ instrument: false, logFunctionCalls: false });
131
+
132
+ const enabled = await runScenario(null);
133
+ assert(
134
+ enabled.includes('parentFn') && enabled.includes('childFn'),
135
+ `expected parentFn + childFn traces when enabled, got: ${enabled.join(', ')}`
136
+ );
137
+
138
+ const disabled = await runScenario([{ fn: 'parentFn' }]);
139
+ assert(
140
+ !disabled.includes('parentFn'),
141
+ `expected parentFn trace to be disabled, got: ${disabled.join(', ')}`
142
+ );
143
+ assert(
144
+ !disabled.includes('childFn'),
145
+ `expected childFn trace to be disabled as descendant, got: ${disabled.join(', ')}`
146
+ );
147
+
148
+ console.log('disableFunctionTraces subtree suppression OK');
149
+ }
150
+
151
+ main().catch(err => {
152
+ console.error(err);
153
+ process.exitCode = 1;
154
+ });
@@ -0,0 +1,183 @@
1
+ // Integration test: unawaited async function traced through reproMiddleware + tracer.
2
+ // Uses actual SDK middleware + tracer runtime, stubbing network posts.
3
+ const assert = require('assert');
4
+ const { EventEmitter } = require('events');
5
+ const { initReproTracing, reproMiddleware } = require('../dist');
6
+ const { trace } = require('../tracer/runtime');
7
+
8
+ // Stub fetch to capture payloads sent by SDK (no real network).
9
+ global.fetch = async (url, opts = {}) => {
10
+ return { ok: true, status: 200, json: async () => ({ ok: true }) };
11
+ };
12
+
13
+ // Fake services and functions that mirror user scenario.
14
+ const studyConfigService = {
15
+ findNotificationModule: async (protocolId) => ({ id: protocolId, cfg: 'notif' }),
16
+ loadStudyModuleConfig: async (protocolId) => ({ id: protocolId, cfg: 'study' }),
17
+ loadStudyConfigUserModule: async (protocolId) => ({ id: protocolId, cfg: 'user' }),
18
+ };
19
+
20
+ const notifyAboutShipmentDispatch = async function notifyAboutShipmentDispatch(protocolId) {
21
+ const notificationConfig = await global.__repro_call(
22
+ studyConfigService.findNotificationModule,
23
+ studyConfigService,
24
+ [protocolId],
25
+ 'app',
26
+ 10,
27
+ 'findNotificationModule',
28
+ false
29
+ );
30
+ const studyModuleConfig = await global.__repro_call(
31
+ studyConfigService.loadStudyModuleConfig,
32
+ studyConfigService,
33
+ [protocolId],
34
+ 'app',
35
+ 11,
36
+ 'loadStudyModuleConfig',
37
+ false
38
+ );
39
+ const userModuleConfig = await global.__repro_call(
40
+ studyConfigService.loadStudyConfigUserModule,
41
+ studyConfigService,
42
+ [protocolId],
43
+ 'app',
44
+ 12,
45
+ 'loadStudyConfigUserModule',
46
+ false
47
+ );
48
+ return { notificationConfig, studyModuleConfig, userModuleConfig };
49
+ };
50
+
51
+ const handleNotificationError = async function handleNotificationError(promise) {
52
+ try { await promise; } catch (err) { return { error: String(err) }; }
53
+ return { ok: true };
54
+ };
55
+
56
+ async function main() {
57
+ // Init tracer before defining app so ALS is ready.
58
+ initReproTracing({ instrument: false, logFunctionCalls: false });
59
+
60
+ const middleware = reproMiddleware({
61
+ appId: 'app',
62
+ appSecret: 'secret',
63
+ captureHeaders: false,
64
+ });
65
+ const handler = middleware;
66
+
67
+ const req = new EventEmitter();
68
+ req.method = 'POST';
69
+ req.url = '/notify';
70
+ req.headers = {
71
+ 'content-type': 'application/json',
72
+ 'x-bug-session-id': 'sid',
73
+ 'x-bug-action-id': 'aid',
74
+ 'x-bug-request-start': String(Date.now()),
75
+ };
76
+ req.body = { protocolId: 'proto-123' };
77
+ const rid = req.headers['x-bug-request-start'];
78
+
79
+ const emitted = [];
80
+ const unsub = trace.on(ev => { emitted.push(ev); });
81
+
82
+ const res = new EventEmitter();
83
+ res.statusCode = 200;
84
+ res.getHeader = () => undefined;
85
+ res.json = function (body) { this.body = body; this.emit('finish'); return body; };
86
+ res.send = function (body) { this.body = body; this.emit('finish'); return body; };
87
+ res.write = function () { return true; };
88
+ res.end = function () { this.emit('finish'); return true; };
89
+
90
+ const route = async () => {
91
+ const protocolId = String((req.body && req.body.protocolId) || 'p1');
92
+ const p = global.__repro_call(
93
+ notifyAboutShipmentDispatch,
94
+ null,
95
+ [protocolId],
96
+ 'app',
97
+ 1,
98
+ 'notifyAboutShipmentDispatch',
99
+ true // mark unawaited
100
+ );
101
+ handleNotificationError(p); // fire-and-forget
102
+ const snapshot = await studyConfigService.findNotificationModule(protocolId);
103
+ res.json({ ok: true, snapshot });
104
+ };
105
+
106
+ await new Promise((resolve, reject) => {
107
+ handler(req, res, async () => {
108
+ try { await route(); resolve(); } catch (err) { reject(err); }
109
+ });
110
+ });
111
+
112
+ await new Promise(r => setTimeout(r, 80)); // allow flush
113
+ unsub();
114
+
115
+ // Rebuild parent/child ordering similar to SDK reorder.
116
+ const interestingFns = new Set([
117
+ 'notifyAboutShipmentDispatch',
118
+ 'findNotificationModule',
119
+ 'loadStudyModuleConfig',
120
+ 'loadStudyConfigUserModule'
121
+ ]);
122
+ const filtered = emitted.filter(ev => interestingFns.has(ev.fn));
123
+
124
+ const normalizeId = v => (v === null || v === undefined ? null : String(v));
125
+ const nodes = new Map();
126
+ const roots = [];
127
+ filtered.forEach((ev, idx) => {
128
+ const sid = normalizeId(ev.spanId);
129
+ const pid = normalizeId(ev.parentSpanId);
130
+ if (!sid) {
131
+ roots.push({ order: idx, ev });
132
+ return;
133
+ }
134
+ let node = nodes.get(sid);
135
+ if (!node) {
136
+ node = { id: sid, parentId: pid, enter: null, exit: null, children: [], order: idx };
137
+ nodes.set(sid, node);
138
+ }
139
+ node.parentId = pid;
140
+ node.order = Math.min(node.order, idx);
141
+ if (ev.type === 'enter' && !node.enter) node.enter = ev;
142
+ if (ev.type === 'exit') node.exit = ev;
143
+ });
144
+
145
+ nodes.forEach(node => {
146
+ if (node.parentId && nodes.has(node.parentId)) {
147
+ nodes.get(node.parentId).children.push(node);
148
+ } else {
149
+ roots.push(node);
150
+ }
151
+ });
152
+ nodes.forEach(node => node.children.sort((a, b) => a.order - b.order));
153
+ roots.sort((a, b) => a.order - b.order);
154
+
155
+ const ordered = [];
156
+ const emitNode = n => {
157
+ if (n.enter) ordered.push(n.enter);
158
+ n.children.forEach(emitNode);
159
+ if (n.exit) ordered.push(n.exit);
160
+ };
161
+ roots.forEach(r => {
162
+ if (r.ev) ordered.push(r.ev); else emitNode(r);
163
+ });
164
+
165
+ const seq = ordered.map(ev => `${ev.type}:${ev.fn}`);
166
+ const expected = [
167
+ 'enter:notifyAboutShipmentDispatch',
168
+ 'enter:findNotificationModule',
169
+ 'exit:findNotificationModule',
170
+ 'enter:loadStudyModuleConfig',
171
+ 'exit:loadStudyModuleConfig',
172
+ 'enter:loadStudyConfigUserModule',
173
+ 'exit:loadStudyConfigUserModule',
174
+ 'exit:notifyAboutShipmentDispatch',
175
+ ];
176
+ assert.deepStrictEqual(seq, expected, `Trace order mismatch.\nGot: ${seq.join(' | ')}\nExpected: ${expected.join(' | ')}`);
177
+ console.log('integration unawaited trace OK');
178
+ }
179
+
180
+ main().catch(err => {
181
+ console.error(err);
182
+ process.exitCode = 1;
183
+ });