@reproapp/node-sdk 0.0.1 → 0.0.2
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/docs/tracing.md +158 -0
- package/package.json +2 -13
- package/src/index.ts +2851 -0
- package/src/integrations/sendgrid.ts +184 -0
- package/tracer/cjs-hook.js +281 -0
- package/tracer/dep-hook.js +142 -0
- package/tracer/esm-loader.mjs +46 -0
- package/tracer/index.js +68 -0
- package/tracer/register.js +194 -0
- package/tracer/runtime.js +963 -0
- package/tracer/server.js +65 -0
- package/tracer/wrap-plugin.js +608 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// repro-node/src/integrations/sendgrid.ts
|
|
2
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
3
|
+
|
|
4
|
+
type Ctx = { sid?: string; aid?: string };
|
|
5
|
+
const als = new AsyncLocalStorage<Ctx>();
|
|
6
|
+
export const getCtx = () => als.getStore() || {};
|
|
7
|
+
|
|
8
|
+
// If you already export als/getCtx from repro-node, reuse that instead of re-declaring.
|
|
9
|
+
|
|
10
|
+
async function post(
|
|
11
|
+
cfg: { appId: string; appSecret: string; appName?: string; apiBase?: string },
|
|
12
|
+
sessionId: string,
|
|
13
|
+
body: any,
|
|
14
|
+
) {
|
|
15
|
+
try {
|
|
16
|
+
const envBase = typeof process !== 'undefined' ? (process as any)?.env?.REPRO_API_BASE : undefined;
|
|
17
|
+
const legacyBase = (cfg as any)?.apiBase;
|
|
18
|
+
const apiBase = String(envBase || legacyBase || 'https://oozy-loreta-gully.ngrok-free.dev').replace(/\/+$/, '');
|
|
19
|
+
await fetch(`${apiBase}/v1/sessions/${sessionId}/backend`, {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: {
|
|
22
|
+
'Content-Type': 'application/json',
|
|
23
|
+
'X-App-Id': cfg.appId,
|
|
24
|
+
'X-App-Secret': cfg.appSecret,
|
|
25
|
+
...(cfg.appName ? { 'X-App-Name': cfg.appName } : {}),
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify(body),
|
|
28
|
+
});
|
|
29
|
+
} catch { /* swallow */ }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type SendgridPatchConfig = {
|
|
33
|
+
appId: string;
|
|
34
|
+
appSecret: string;
|
|
35
|
+
appName?: string;
|
|
36
|
+
// Optional: provide a function to resolve sid/aid if AsyncLocalStorage is not set
|
|
37
|
+
resolveContext?: () => { sid?: string; aid?: string } | undefined;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Patch @sendgrid/mail's send() and sendMultiple() to capture outbound email.
|
|
42
|
+
* Idempotent. No behavior change for the app.
|
|
43
|
+
*/
|
|
44
|
+
export function patchSendgridMail(cfg: SendgridPatchConfig) {
|
|
45
|
+
let sgMail: any;
|
|
46
|
+
try { sgMail = require('@sendgrid/mail'); } catch { return; } // not installed → no-op
|
|
47
|
+
|
|
48
|
+
if (!sgMail || (sgMail as any).__repro_patched) return;
|
|
49
|
+
(sgMail as any).__repro_patched = true;
|
|
50
|
+
|
|
51
|
+
const origSend = sgMail.send?.bind(sgMail);
|
|
52
|
+
const origSendMultiple = sgMail.sendMultiple?.bind(sgMail);
|
|
53
|
+
|
|
54
|
+
if (origSend) {
|
|
55
|
+
sgMail.send = async function patchedSend(msg: any, isMultiple?: boolean) {
|
|
56
|
+
const startedAt = Date.now();
|
|
57
|
+
let statusCode: number | undefined;
|
|
58
|
+
let headers: Record<string, any> | undefined;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const res = await origSend(msg, isMultiple);
|
|
62
|
+
// sendgrid returns [response] as array in v7.x
|
|
63
|
+
const r = Array.isArray(res) ? res[0] : res;
|
|
64
|
+
statusCode = r?.statusCode ?? r?.status ?? undefined;
|
|
65
|
+
headers = r?.headers ?? undefined;
|
|
66
|
+
return res;
|
|
67
|
+
} finally {
|
|
68
|
+
fireCapture('send', msg, startedAt, statusCode, headers);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (origSendMultiple) {
|
|
74
|
+
sgMail.sendMultiple = async function patchedSendMultiple(msg: any) {
|
|
75
|
+
const startedAt = Date.now();
|
|
76
|
+
let statusCode: number | undefined;
|
|
77
|
+
let headers: Record<string, any> | undefined;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const res = await origSendMultiple(msg);
|
|
81
|
+
const r = Array.isArray(res) ? res[0] : res;
|
|
82
|
+
statusCode = r?.statusCode ?? r?.status ?? undefined;
|
|
83
|
+
headers = r?.headers ?? undefined;
|
|
84
|
+
return res;
|
|
85
|
+
} finally {
|
|
86
|
+
fireCapture('sendMultiple', msg, startedAt, statusCode, headers);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function fireCapture(kind: 'send' | 'sendMultiple', rawMsg: any, t0: number, statusCode?: number, headers?: any) {
|
|
92
|
+
const ctx = getCtx();
|
|
93
|
+
const sid = ctx.sid ?? cfg.resolveContext?.()?.sid;
|
|
94
|
+
const aid = ctx.aid ?? cfg.resolveContext?.()?.aid;
|
|
95
|
+
if (!sid) return; // no active session → skip
|
|
96
|
+
|
|
97
|
+
const norm = normalizeSendgridMessage(rawMsg);
|
|
98
|
+
const entry = {
|
|
99
|
+
actionId: aid ?? null,
|
|
100
|
+
email: {
|
|
101
|
+
provider: 'sendgrid',
|
|
102
|
+
kind,
|
|
103
|
+
to: norm.to,
|
|
104
|
+
cc: norm.cc,
|
|
105
|
+
bcc: norm.bcc,
|
|
106
|
+
from: norm.from,
|
|
107
|
+
subject: norm.subject,
|
|
108
|
+
text: norm.text, // you said privacy later → include now
|
|
109
|
+
html: norm.html, // idem
|
|
110
|
+
templateId: norm.templateId,
|
|
111
|
+
dynamicTemplateData: norm.dynamicTemplateData,
|
|
112
|
+
categories: norm.categories,
|
|
113
|
+
customArgs: norm.customArgs,
|
|
114
|
+
attachmentsMeta: norm.attachmentsMeta, // safe metadata only
|
|
115
|
+
statusCode,
|
|
116
|
+
durMs: Date.now() - t0,
|
|
117
|
+
headers: headers ?? {},
|
|
118
|
+
},
|
|
119
|
+
t: Date.now(),
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
post(cfg, sid, { entries: [entry] });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizeAddress(a: any): { email: string; name?: string } | null {
|
|
126
|
+
if (!a) return null;
|
|
127
|
+
if (typeof a === 'string') return { email: a };
|
|
128
|
+
if (typeof a === 'object' && a.email) return { email: String(a.email), name: a.name ? String(a.name) : undefined };
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function normalizeAddressList(v: any): Array<{ email: string; name?: string }> | undefined {
|
|
133
|
+
if (!v) return undefined;
|
|
134
|
+
const arr = Array.isArray(v) ? v : [v];
|
|
135
|
+
const out = arr.map(normalizeAddress).filter(Boolean) as Array<{ email: string; name?: string }>;
|
|
136
|
+
return out.length ? out : undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function normalizeSendgridMessage(msg: any) {
|
|
140
|
+
// sendgrid supports "personalizations" & top-level fields; we’ll flatten the common pieces
|
|
141
|
+
const base = {
|
|
142
|
+
from: normalizeAddress(msg?.from) ?? undefined,
|
|
143
|
+
to: normalizeAddressList(msg?.to),
|
|
144
|
+
cc: normalizeAddressList(msg?.cc),
|
|
145
|
+
bcc: normalizeAddressList(msg?.bcc),
|
|
146
|
+
subject: msg?.subject ? String(msg.subject) : undefined,
|
|
147
|
+
text: typeof msg?.text === 'string' ? msg.text : undefined,
|
|
148
|
+
html: typeof msg?.html === 'string' ? msg.html : undefined,
|
|
149
|
+
templateId: msg?.templateId ? String(msg.templateId) : undefined,
|
|
150
|
+
dynamicTemplateData: msg?.dynamic_template_data ?? msg?.dynamicTemplateData ?? undefined,
|
|
151
|
+
categories: Array.isArray(msg?.categories) ? msg.categories.map(String) : undefined,
|
|
152
|
+
customArgs: msg?.customArgs ?? msg?.custom_args ?? undefined,
|
|
153
|
+
attachmentsMeta: Array.isArray(msg?.attachments)
|
|
154
|
+
? msg.attachments.map((a: any) => ({
|
|
155
|
+
filename: a?.filename ? String(a.filename) : undefined,
|
|
156
|
+
type: a?.type ? String(a.type) : undefined,
|
|
157
|
+
size: a?.content ? byteLen(a.content) : undefined, // base64 or string → approximate size
|
|
158
|
+
}))
|
|
159
|
+
: undefined,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// If personalizations exist, pull the FIRST one as representative (keeps MVP simple)
|
|
163
|
+
const p0 = Array.isArray(msg?.personalizations) ? msg.personalizations[0] : undefined;
|
|
164
|
+
if (p0) {
|
|
165
|
+
base.to = normalizeAddressList(p0.to) ?? base.to;
|
|
166
|
+
base.cc = normalizeAddressList(p0.cc) ?? base.cc;
|
|
167
|
+
base.bcc = normalizeAddressList(p0.bcc) ?? base.bcc;
|
|
168
|
+
if (!base.subject && p0.subject) base.subject = String(p0.subject);
|
|
169
|
+
// template data can also live inside personalization
|
|
170
|
+
if (!base.dynamicTemplateData && p0.dynamic_template_data) base.dynamicTemplateData = p0.dynamic_template_data;
|
|
171
|
+
if (!base.customArgs && p0.custom_args) base.customArgs = p0.custom_args;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return base;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function byteLen(content: any): number | undefined {
|
|
178
|
+
try {
|
|
179
|
+
if (typeof content === 'string') return Buffer.byteLength(content, 'utf8');
|
|
180
|
+
if (content && typeof content === 'object' && 'length' in content) return Number((content as any).length);
|
|
181
|
+
} catch {}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// cjs-hook.js
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const Module = require('node:module');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const babel = require('@babel/core');
|
|
6
|
+
const makeWrap = require('./wrap-plugin');
|
|
7
|
+
const { TraceMap, originalPositionFor } = require('@jridgewell/trace-mapping');
|
|
8
|
+
const { SYM_SRC_FILE, SYM_IS_APP, SYM_BODY_TRACED } = require('./runtime');
|
|
9
|
+
|
|
10
|
+
const CWD = process.cwd().replace(/\\/g, '/');
|
|
11
|
+
const escapeRx = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
12
|
+
|
|
13
|
+
// app file check (your existing logic)
|
|
14
|
+
function isAppFile(filename) {
|
|
15
|
+
const f = String(filename || '').replace(/\\/g, '/');
|
|
16
|
+
return f.startsWith(CWD + '/') && !f.includes('/node_modules/');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function toPosix(file) {
|
|
20
|
+
return String(file || '').replace(/\\/g, '/');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function tagExports(value, filename, seen = new WeakSet(), depth = 0, instrumented = false) {
|
|
24
|
+
if (value == null) return;
|
|
25
|
+
const ty = typeof value;
|
|
26
|
+
if (ty !== 'object' && ty !== 'function') return;
|
|
27
|
+
if (seen.has(value)) return;
|
|
28
|
+
seen.add(value);
|
|
29
|
+
|
|
30
|
+
const isApp = isAppFile(filename);
|
|
31
|
+
|
|
32
|
+
if (typeof value === 'function') {
|
|
33
|
+
try {
|
|
34
|
+
if (!value[SYM_SRC_FILE]) {
|
|
35
|
+
Object.defineProperty(value, SYM_SRC_FILE, { value: filename, configurable: true });
|
|
36
|
+
}
|
|
37
|
+
if (value[SYM_IS_APP] !== isApp) {
|
|
38
|
+
Object.defineProperty(value, SYM_IS_APP, { value: isApp, configurable: true });
|
|
39
|
+
}
|
|
40
|
+
if (instrumented && value.__repro_instrumented !== true) {
|
|
41
|
+
Object.defineProperty(value, '__repro_instrumented', { value: true, configurable: true });
|
|
42
|
+
}
|
|
43
|
+
if (instrumented && value[SYM_BODY_TRACED] !== true) {
|
|
44
|
+
Object.defineProperty(value, SYM_BODY_TRACED, { value: true, configurable: true });
|
|
45
|
+
}
|
|
46
|
+
} catch {}
|
|
47
|
+
const proto = value.prototype;
|
|
48
|
+
if (proto && typeof proto === 'object') {
|
|
49
|
+
for (const k of Object.getOwnPropertyNames(proto)) {
|
|
50
|
+
if (k === 'constructor') continue;
|
|
51
|
+
const d = Object.getOwnPropertyDescriptor(proto, k);
|
|
52
|
+
if (!d) continue;
|
|
53
|
+
if (typeof d.value === 'function') tagExports(d.value, filename, seen, depth + 1, instrumented);
|
|
54
|
+
// also tag accessors
|
|
55
|
+
if (typeof d.get === 'function') tagExports(d.get, filename, seen, depth + 1, instrumented);
|
|
56
|
+
if (typeof d.set === 'function') tagExports(d.set, filename, seen, depth + 1, instrumented);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (typeof value === 'object' && depth < 4) {
|
|
62
|
+
for (const k of Object.getOwnPropertyNames(value)) {
|
|
63
|
+
const d = Object.getOwnPropertyDescriptor(value, k);
|
|
64
|
+
if (!d) continue;
|
|
65
|
+
|
|
66
|
+
if ('value' in d) tagExports(d.value, filename, seen, depth + 1, instrumented);
|
|
67
|
+
|
|
68
|
+
if (typeof d.get === 'function') {
|
|
69
|
+
tagExports(d.get, filename, seen, depth + 1, instrumented);
|
|
70
|
+
try { tagExports(value[k], filename, seen, depth + 1, instrumented); } catch {}
|
|
71
|
+
}
|
|
72
|
+
if (typeof d.set === 'function') {
|
|
73
|
+
tagExports(d.set, filename, seen, depth + 1, instrumented);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function installCJS({ include, exclude, parserPlugins } = {}) {
|
|
80
|
+
// default include = project dir; exclude node_modules
|
|
81
|
+
const inc = Array.isArray(include) && include.length
|
|
82
|
+
? include
|
|
83
|
+
: [ new RegExp('^' + escapeRx(CWD + '/')) ];
|
|
84
|
+
const exc = Array.isArray(exclude) ? exclude : [];
|
|
85
|
+
|
|
86
|
+
const shouldHandle = (f) => {
|
|
87
|
+
const s = String(f || '').replace(/\\/g, '/');
|
|
88
|
+
if (exc.some(rx => rx.test(s))) return false;
|
|
89
|
+
return inc.some(rx => rx.test(s));
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// ---- Global hook: intercept *all* compiles, regardless of how .ts was compiled ----
|
|
93
|
+
const origCompile = Module.prototype._compile;
|
|
94
|
+
const transformedFiles = new Set();
|
|
95
|
+
Module.prototype._compile = function patchedCompile(code, filename) {
|
|
96
|
+
let out = code;
|
|
97
|
+
let metaFilename = filename;
|
|
98
|
+
let mapOriginalPosition = null;
|
|
99
|
+
let wasInstrumented = false;
|
|
100
|
+
try {
|
|
101
|
+
if (transformedFiles.has(filename)) {
|
|
102
|
+
// Already transformed once; avoid double-transforming code that may already include __repro_call wrappers.
|
|
103
|
+
out = code;
|
|
104
|
+
} else if (shouldHandle(filename) && isAppFile(filename)) {
|
|
105
|
+
try { process.stderr.write(`[trace-debug] compile: ${filename}\n`); } catch {}
|
|
106
|
+
const sourceInfo = getSourceInfo(code, filename);
|
|
107
|
+
if (sourceInfo?.metaFilename) {
|
|
108
|
+
metaFilename = sourceInfo.metaFilename;
|
|
109
|
+
}
|
|
110
|
+
if (typeof sourceInfo?.mapOriginalPosition === 'function') {
|
|
111
|
+
mapOriginalPosition = sourceInfo.mapOriginalPosition;
|
|
112
|
+
}
|
|
113
|
+
// Transform the already-compiled JS (keeps Nest’s decorator metadata intact)
|
|
114
|
+
const res = babel.transformSync(code, {
|
|
115
|
+
filename,
|
|
116
|
+
sourceType: 'unambiguous',
|
|
117
|
+
retainLines: true,
|
|
118
|
+
sourceMaps: 'inline',
|
|
119
|
+
parserOpts: {
|
|
120
|
+
sourceType: 'unambiguous',
|
|
121
|
+
// We’re parsing JS here; TS was compiled by ts-node/Nest.
|
|
122
|
+
plugins: parserPlugins || [
|
|
123
|
+
'jsx', 'classProperties', 'classPrivateProperties',
|
|
124
|
+
'classPrivateMethods', 'dynamicImport', 'topLevelAwait',
|
|
125
|
+
'optionalChaining', 'nullishCoalescingOperator',
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
// only the wrap plugin; do NOT run TS transform here
|
|
129
|
+
plugins: [
|
|
130
|
+
[ makeWrap(metaFilename, {
|
|
131
|
+
mode: 'all',
|
|
132
|
+
wrapGettersSetters: false,
|
|
133
|
+
skipAnonymous: false,
|
|
134
|
+
mapOriginalPosition,
|
|
135
|
+
}) ],
|
|
136
|
+
],
|
|
137
|
+
compact: false,
|
|
138
|
+
comments: true,
|
|
139
|
+
});
|
|
140
|
+
out = res?.code || code;
|
|
141
|
+
wasInstrumented = !!res?.code;
|
|
142
|
+
transformedFiles.add(filename);
|
|
143
|
+
}
|
|
144
|
+
} catch (err) {
|
|
145
|
+
out = code; // never break the app if transform fails
|
|
146
|
+
try { process.stderr.write(`[trace-debug] compile error: ${filename} :: ${err?.message || err}\n`); } catch {}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const ret = origCompile.call(this, out, filename);
|
|
150
|
+
|
|
151
|
+
// Tag exports for origin detection
|
|
152
|
+
try { tagExports(this.exports, metaFilename, new WeakSet(), 0, wasInstrumented); } catch {}
|
|
153
|
+
try { this.__repro_wrapped = true; } catch {}
|
|
154
|
+
|
|
155
|
+
return ret;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// keep your load wrapper to tag modules loaded in other ways too
|
|
159
|
+
const _resolveFilename = Module._resolveFilename;
|
|
160
|
+
const _load = Module._load;
|
|
161
|
+
Module._load = function patchedLoad(request, parent, isMain) {
|
|
162
|
+
const filename = (() => {
|
|
163
|
+
try { return _resolveFilename.call(Module, request, parent, isMain); }
|
|
164
|
+
catch { return String(request); } // e.g., 'node:fs'
|
|
165
|
+
})();
|
|
166
|
+
const exp = _load.apply(this, arguments);
|
|
167
|
+
try { tagExports(exp, filename); } catch {}
|
|
168
|
+
return exp;
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function getSourceInfo(code, filename) {
|
|
173
|
+
try {
|
|
174
|
+
const map = loadSourceMap(code, filename);
|
|
175
|
+
if (!map) return null;
|
|
176
|
+
|
|
177
|
+
const mapFile = map.__mapFile || filename;
|
|
178
|
+
const { sources = [], sourceRoot } = map;
|
|
179
|
+
const resolvedSourceRoot = sourceRoot
|
|
180
|
+
? resolveSourcePath(sourceRoot, mapFile)
|
|
181
|
+
: '';
|
|
182
|
+
|
|
183
|
+
let metaFilename = null;
|
|
184
|
+
for (const src of sources) {
|
|
185
|
+
if (!src) continue;
|
|
186
|
+
const abs = resolveSourcePath(src, mapFile, resolvedSourceRoot);
|
|
187
|
+
if (!abs) continue;
|
|
188
|
+
metaFilename = toPosix(abs);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const mapper = makeOriginalPositionMapper(map, mapFile, resolvedSourceRoot);
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
metaFilename,
|
|
196
|
+
mapOriginalPosition: mapper,
|
|
197
|
+
};
|
|
198
|
+
} catch {}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function makeOriginalPositionMapper(map, mapFile, resolvedSourceRoot) {
|
|
203
|
+
try {
|
|
204
|
+
const traceMap = new TraceMap(map);
|
|
205
|
+
return (line, column = 0) => {
|
|
206
|
+
if (line == null) return null;
|
|
207
|
+
try {
|
|
208
|
+
const original = originalPositionFor(traceMap, { line, column });
|
|
209
|
+
if (!original || original.line == null) return null;
|
|
210
|
+
|
|
211
|
+
const file = original.source
|
|
212
|
+
? toPosix(resolveSourcePath(original.source, mapFile, resolvedSourceRoot))
|
|
213
|
+
: null;
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
line: original.line,
|
|
217
|
+
column: original.column ?? 0,
|
|
218
|
+
file,
|
|
219
|
+
};
|
|
220
|
+
} catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
} catch {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function resolveSourcePath(sourcePath, relativeTo, rootOverride) {
|
|
230
|
+
try {
|
|
231
|
+
const baseDir = path.dirname(relativeTo);
|
|
232
|
+
const combined = rootOverride
|
|
233
|
+
? path.resolve(rootOverride, sourcePath)
|
|
234
|
+
: path.resolve(baseDir, sourcePath);
|
|
235
|
+
return combined;
|
|
236
|
+
} catch {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function loadSourceMap(code, filename) {
|
|
242
|
+
const match = /\/\/[#@]\s*sourceMappingURL=([^\s]+)/.exec(code);
|
|
243
|
+
if (!match) return null;
|
|
244
|
+
|
|
245
|
+
const url = match[1].trim();
|
|
246
|
+
if (!url) return null;
|
|
247
|
+
|
|
248
|
+
if (url.startsWith('data:')) {
|
|
249
|
+
return parseDataUrl(url);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const mapPath = path.resolve(path.dirname(filename), url);
|
|
253
|
+
try {
|
|
254
|
+
const text = fs.readFileSync(mapPath, 'utf8');
|
|
255
|
+
const json = JSON.parse(text);
|
|
256
|
+
Object.defineProperty(json, '__mapFile', { value: mapPath });
|
|
257
|
+
return json;
|
|
258
|
+
} catch {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function parseDataUrl(url) {
|
|
264
|
+
const comma = url.indexOf(',');
|
|
265
|
+
if (comma < 0) return null;
|
|
266
|
+
|
|
267
|
+
const meta = url.slice(5, comma);
|
|
268
|
+
const data = url.slice(comma + 1);
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
if (/;base64/i.test(meta)) {
|
|
272
|
+
const buf = Buffer.from(data, 'base64');
|
|
273
|
+
return JSON.parse(buf.toString('utf8'));
|
|
274
|
+
}
|
|
275
|
+
return JSON.parse(decodeURIComponent(data));
|
|
276
|
+
} catch {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
module.exports = { installCJS };
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// dep-hook.js
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const shimmer = require('shimmer');
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
SYM_SKIP_WRAP
|
|
9
|
+
} = require('./runtime');
|
|
10
|
+
|
|
11
|
+
// ---- config / guards ----
|
|
12
|
+
const CWD = process.cwd().replace(/\\/g, '/');
|
|
13
|
+
const isOurFile = (f) => f && f.replace(/\\/g,'/').includes('/omnitrace/') || f.includes('/repro/');
|
|
14
|
+
const isInNodeModules = (f) => f && f.replace(/\\/g,'/').includes('/node_modules/');
|
|
15
|
+
|
|
16
|
+
const SKIP_METHOD_NAMES = new Set([
|
|
17
|
+
// thenables + common promise-ish
|
|
18
|
+
'then','catch','finally',
|
|
19
|
+
// mongoose query/aggregate “execute” hooks — we log via schema middleware
|
|
20
|
+
'exec',
|
|
21
|
+
// avoid patching Node’s Symbol-based internals
|
|
22
|
+
Symbol.toStringTag
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
// Don’t double-wrap
|
|
26
|
+
function alreadyWrapped(fn) { return !!(fn && fn.__repro_wrapped); }
|
|
27
|
+
function markWrapped(fn) { try { Object.defineProperty(fn, '__repro_wrapped', { value: true }); } catch {} }
|
|
28
|
+
|
|
29
|
+
// our call bridge -> uses your global helper, preserves return value identity
|
|
30
|
+
function wrapFunction(original, label, file, line) {
|
|
31
|
+
if (typeof original !== 'function') return original;
|
|
32
|
+
if (alreadyWrapped(original)) return original;
|
|
33
|
+
|
|
34
|
+
const wrapped = function reproWrapped() {
|
|
35
|
+
// Use the call-site shim to classify app/dep and to safely handle thenables
|
|
36
|
+
return global.__repro_call
|
|
37
|
+
? global.__repro_call(original, this, Array.from(arguments), file, line, label || original.name || '', false)
|
|
38
|
+
: original.apply(this, arguments);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// copy a few common props (name/length are non-writable; don’t force)
|
|
42
|
+
try { wrapped[SYM_SKIP_WRAP] = true; } catch {}
|
|
43
|
+
try { Object.defineProperty(wrapped, '__repro_instrumented', { value: true, configurable: true }); } catch {}
|
|
44
|
+
markWrapped(wrapped);
|
|
45
|
+
return wrapped;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function wrapObjectMethods(obj, file) {
|
|
49
|
+
const seen = new WeakSet();
|
|
50
|
+
const maxDepth = 4;
|
|
51
|
+
|
|
52
|
+
const visit = (target, depth = 0) => {
|
|
53
|
+
if (!target || (typeof target !== 'object' && typeof target !== 'function')) return;
|
|
54
|
+
if (seen.has(target)) return;
|
|
55
|
+
if (depth > maxDepth) return;
|
|
56
|
+
seen.add(target);
|
|
57
|
+
|
|
58
|
+
// own props
|
|
59
|
+
for (const k of Object.getOwnPropertyNames(target)) {
|
|
60
|
+
const d = Object.getOwnPropertyDescriptor(target, k);
|
|
61
|
+
if (!d) continue;
|
|
62
|
+
if (d.get || d.set) continue; // never wrap accessors
|
|
63
|
+
if (SKIP_METHOD_NAMES.has(k)) continue;
|
|
64
|
+
|
|
65
|
+
if (typeof d.value === 'function') {
|
|
66
|
+
const v = d.value;
|
|
67
|
+
if (!alreadyWrapped(v)) {
|
|
68
|
+
const w = wrapFunction(v, String(k), file, 0);
|
|
69
|
+
try { shimmer.wrap(target, k, () => w); } catch {
|
|
70
|
+
try { target[k] = w; } catch {}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (d.value && (typeof d.value === 'object' || typeof d.value === 'function')) {
|
|
77
|
+
visit(d.value, depth + 1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// class prototype methods (walk depth once)
|
|
82
|
+
const proto = Object.getPrototypeOf(target);
|
|
83
|
+
if (proto && proto !== Object.prototype) {
|
|
84
|
+
visit(proto, depth + 1);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
visit(obj, 0);
|
|
89
|
+
return obj;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function shouldInstrument(filename, moduleName) {
|
|
93
|
+
const f = String(filename || '').replace(/\\/g,'/');
|
|
94
|
+
|
|
95
|
+
if (!f) return false;
|
|
96
|
+
if (isOurFile(f)) return false; // never instrument tracer itself
|
|
97
|
+
if (f.includes('/@babel/')) return false; // avoid babel internals
|
|
98
|
+
if (!isInNodeModules(f) && !f.startsWith(CWD + '/')) { // odd cases
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function instrumentExports(exports, filename, moduleName) {
|
|
105
|
+
if (!shouldInstrument(filename, moduleName)) return exports;
|
|
106
|
+
|
|
107
|
+
// Avoid breaking common patterns:
|
|
108
|
+
// - Don’t mutate Mongoose core types (Query/Aggregate) — you already log via schema hooks
|
|
109
|
+
try {
|
|
110
|
+
if (moduleName === 'mongoose' || /[\\/]mongoose[\\/]/.test(filename)) {
|
|
111
|
+
// wrap top-level exported functions only; skip prototype types
|
|
112
|
+
if (exports && typeof exports === 'object') {
|
|
113
|
+
for (const k of Object.getOwnPropertyNames(exports)) {
|
|
114
|
+
const d = Object.getOwnPropertyDescriptor(exports, k);
|
|
115
|
+
if (!d || d.get || d.set) continue;
|
|
116
|
+
if (typeof d.value === 'function' && !SKIP_METHOD_NAMES.has(k)) {
|
|
117
|
+
const w = wrapFunction(d.value, `${moduleName}.${k}`, filename, 0);
|
|
118
|
+
try { shimmer.wrap(exports, k, () => w); } catch {}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return exports;
|
|
123
|
+
}
|
|
124
|
+
} catch {}
|
|
125
|
+
|
|
126
|
+
// Generic: recursively wrap functions on object exports
|
|
127
|
+
try {
|
|
128
|
+
if (typeof exports === 'function') {
|
|
129
|
+
const wrapped = wrapFunction(exports, moduleName || path.basename(filename), filename, 0);
|
|
130
|
+
try { wrapObjectMethods(wrapped.prototype, filename); } catch {}
|
|
131
|
+
return wrapped;
|
|
132
|
+
}
|
|
133
|
+
if (exports && typeof exports === 'object') {
|
|
134
|
+
const wrappedObj = wrapObjectMethods(exports, filename);
|
|
135
|
+
return wrappedObj;
|
|
136
|
+
}
|
|
137
|
+
} catch {}
|
|
138
|
+
|
|
139
|
+
return exports;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = { instrumentExports };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// esm-loader.mjs
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
import * as babel from '@babel/core';
|
|
5
|
+
import tsPlugin from '@babel/plugin-transform-typescript';
|
|
6
|
+
import makeWrap from './wrap-plugin.js';
|
|
7
|
+
|
|
8
|
+
const parserPlugins = [
|
|
9
|
+
'jsx',
|
|
10
|
+
['decorators', { version: 'legacy' }], // adjust per your stack
|
|
11
|
+
'classProperties','classPrivateProperties','classPrivateMethods',
|
|
12
|
+
'dynamicImport','topLevelAwait','typescript'
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
// naive include/exclude based on CWD by default
|
|
16
|
+
const CWD = process.cwd().replace(/\\/g,'/');
|
|
17
|
+
const include = [ new RegExp('^' + CWD.replace(/[.*+?^${}()|[\\]\\\\]/g,'\\$&') + '/') ];
|
|
18
|
+
const exclude = [ /node_modules[\\/]/ ];
|
|
19
|
+
|
|
20
|
+
export async function resolve(specifier, context, next) {
|
|
21
|
+
const r = await next(specifier, context);
|
|
22
|
+
return r; // default resolution
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function load(url, context, next) {
|
|
26
|
+
const r = await next(url, context);
|
|
27
|
+
if (r.format !== 'module' && r.format !== 'commonjs') return r;
|
|
28
|
+
|
|
29
|
+
const filename = url.startsWith('file:') ? fileURLToPath(url) : null;
|
|
30
|
+
if (!filename) return r;
|
|
31
|
+
|
|
32
|
+
const s = filename.replace(/\\/g,'/');
|
|
33
|
+
if (exclude.some(rx => rx.test(s)) || !include.some(rx => rx.test(s))) return r;
|
|
34
|
+
|
|
35
|
+
const code = await readFile(filename, 'utf8');
|
|
36
|
+
const out = babel.transformSync(code, {
|
|
37
|
+
filename,
|
|
38
|
+
sourceType: 'unambiguous',
|
|
39
|
+
retainLines: true,
|
|
40
|
+
parserOpts: { sourceType:'unambiguous', plugins: parserPlugins },
|
|
41
|
+
plugins: [[ makeWrap(filename) ], [ tsPlugin, { allowDeclareFields:true } ]],
|
|
42
|
+
sourceMaps: 'inline',
|
|
43
|
+
})?.code || code;
|
|
44
|
+
|
|
45
|
+
return { format: r.format, source: out };
|
|
46
|
+
}
|