evergreen-sdk 1.0.1 → 1.0.3
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.d.ts +9 -1
- package/dist/index.js +77 -6
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +37 -0
- package/package.json +1 -1
- package/src/index.test.ts +53 -0
- package/src/index.ts +82 -5
- package/src/index.js +0 -160
package/dist/index.d.ts
CHANGED
|
@@ -2,15 +2,23 @@ interface ApiCallMeta {
|
|
|
2
2
|
callee: string;
|
|
3
3
|
arg0?: string;
|
|
4
4
|
}
|
|
5
|
+
interface RunOptions {
|
|
6
|
+
callTrace?: {
|
|
7
|
+
include?: string[];
|
|
8
|
+
exclude?: string[];
|
|
9
|
+
};
|
|
10
|
+
}
|
|
5
11
|
interface StepOptions {
|
|
6
12
|
input?: any;
|
|
7
13
|
children?: string[];
|
|
8
14
|
apiCalls?: ApiCallMeta[];
|
|
9
15
|
__not_async?: boolean;
|
|
10
16
|
}
|
|
17
|
+
declare function runWithContext<T>(jobId: string, fn: () => T): T;
|
|
18
|
+
declare function runWithContext<T>(jobId: string, options: RunOptions, fn: () => T): T;
|
|
11
19
|
export declare const evergreen: {
|
|
12
20
|
clear(): void;
|
|
13
|
-
run
|
|
21
|
+
run: typeof runWithContext;
|
|
14
22
|
__autoStep<T>(name: string, location: {
|
|
15
23
|
file: string;
|
|
16
24
|
line: number;
|
package/dist/index.js
CHANGED
|
@@ -10,15 +10,44 @@ const traceStorage = new node_async_hooks_1.AsyncLocalStorage();
|
|
|
10
10
|
let ws = null;
|
|
11
11
|
let isConnected = false;
|
|
12
12
|
const pendingTelemetry = [];
|
|
13
|
+
function getTelemetryWsUrl() {
|
|
14
|
+
const explicit = process.env.EVERGREEN_TRACE_WS_URL;
|
|
15
|
+
if (explicit && explicit.trim())
|
|
16
|
+
return explicit.trim();
|
|
17
|
+
return "ws://localhost:4321";
|
|
18
|
+
}
|
|
19
|
+
function shouldDebugTelemetryConnection() {
|
|
20
|
+
return (process.env.EVERGREEN_TRACE_DEBUG === "1" ||
|
|
21
|
+
process.env.EVERGREEN_TRACE_DEBUG === "true");
|
|
22
|
+
}
|
|
23
|
+
function shouldDisableWs() {
|
|
24
|
+
// In tests, keeping a websocket instance around can keep the Node test runner alive.
|
|
25
|
+
if (process.env.NODE_ENV === "test")
|
|
26
|
+
return true;
|
|
27
|
+
return (process.env.EVERGREEN_TRACE_DISABLE_WS === "1" ||
|
|
28
|
+
process.env.EVERGREEN_TRACE_DISABLE_WS === "true");
|
|
29
|
+
}
|
|
13
30
|
// Connect to the VS Code Extension Hub
|
|
14
31
|
function connect() {
|
|
32
|
+
if (shouldDisableWs())
|
|
33
|
+
return;
|
|
15
34
|
if (ws &&
|
|
16
35
|
(ws.readyState === ws_1.default.CONNECTING || ws.readyState === ws_1.default.OPEN))
|
|
17
36
|
return;
|
|
37
|
+
const url = getTelemetryWsUrl();
|
|
38
|
+
const debug = shouldDebugTelemetryConnection();
|
|
18
39
|
try {
|
|
19
|
-
|
|
40
|
+
if (debug) {
|
|
41
|
+
// eslint-disable-next-line no-console
|
|
42
|
+
console.log(`🌲 Evergreen Trace: connecting to ${url}`);
|
|
43
|
+
}
|
|
44
|
+
ws = new ws_1.default(url);
|
|
20
45
|
ws.on("open", () => {
|
|
21
46
|
isConnected = true;
|
|
47
|
+
if (debug) {
|
|
48
|
+
// eslint-disable-next-line no-console
|
|
49
|
+
console.log(`🌲 Evergreen Trace: connected to ${url}`);
|
|
50
|
+
}
|
|
22
51
|
// Flush any telemetry that was emitted before the socket became ready.
|
|
23
52
|
while (pendingTelemetry.length > 0) {
|
|
24
53
|
const queued = pendingTelemetry.shift();
|
|
@@ -75,6 +104,41 @@ function sendTelemetry(payload) {
|
|
|
75
104
|
// Fire and forget, silently ignore send errors
|
|
76
105
|
}
|
|
77
106
|
}
|
|
107
|
+
function escapeRegexLiteral(input) {
|
|
108
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
109
|
+
}
|
|
110
|
+
function matchesPattern(value, pattern) {
|
|
111
|
+
// Support:
|
|
112
|
+
// - exact match: "app.listen"
|
|
113
|
+
// - simple wildcard: "redis.*" or "prisma.*.findMany" etc.
|
|
114
|
+
if (!pattern)
|
|
115
|
+
return false;
|
|
116
|
+
if (!pattern.includes("*"))
|
|
117
|
+
return value === pattern;
|
|
118
|
+
const re = new RegExp("^" +
|
|
119
|
+
pattern
|
|
120
|
+
.split("*")
|
|
121
|
+
.map((part) => escapeRegexLiteral(part))
|
|
122
|
+
.join(".*") +
|
|
123
|
+
"$");
|
|
124
|
+
return re.test(value);
|
|
125
|
+
}
|
|
126
|
+
function filterApiCallsForRun(apiCalls, store) {
|
|
127
|
+
if (!apiCalls || apiCalls.length === 0)
|
|
128
|
+
return [];
|
|
129
|
+
const include = store.callTrace?.include?.filter(Boolean) ?? [];
|
|
130
|
+
const exclude = store.callTrace?.exclude?.filter(Boolean) ?? [];
|
|
131
|
+
return apiCalls.filter((c) => {
|
|
132
|
+
if (exclude.length && exclude.some((p) => matchesPattern(c.callee, p))) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
if (include.length) {
|
|
136
|
+
return include.some((p) => matchesPattern(c.callee, p));
|
|
137
|
+
}
|
|
138
|
+
// default: include everything that was collected
|
|
139
|
+
return true;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
78
142
|
function extractLocationFromStack(stack) {
|
|
79
143
|
if (!stack)
|
|
80
144
|
return undefined;
|
|
@@ -93,13 +157,19 @@ function extractLocationFromStack(stack) {
|
|
|
93
157
|
}
|
|
94
158
|
return undefined;
|
|
95
159
|
}
|
|
160
|
+
function runWithContext(jobId, a, b) {
|
|
161
|
+
const fn = typeof a === "function" ? a : b;
|
|
162
|
+
const options = typeof a === "function" ? undefined : a;
|
|
163
|
+
if (!fn) {
|
|
164
|
+
throw new Error("evergreen.run(jobId, fn) requires a function");
|
|
165
|
+
}
|
|
166
|
+
return traceStorage.run({ jobId, callTrace: options?.callTrace }, fn);
|
|
167
|
+
}
|
|
96
168
|
exports.evergreen = {
|
|
97
169
|
clear() {
|
|
98
170
|
sendTelemetry({ type: "CLEAR" });
|
|
99
171
|
},
|
|
100
|
-
run
|
|
101
|
-
return traceStorage.run({ jobId }, fn);
|
|
102
|
-
},
|
|
172
|
+
run: runWithContext,
|
|
103
173
|
async __autoStep(name, location, fn, options) {
|
|
104
174
|
const store = traceStorage.getStore();
|
|
105
175
|
// If no context, just execute the function normally without tracing
|
|
@@ -107,6 +177,7 @@ exports.evergreen = {
|
|
|
107
177
|
return await fn();
|
|
108
178
|
}
|
|
109
179
|
const { jobId, previousStep } = store;
|
|
180
|
+
const filteredApiCalls = filterApiCallsForRun(options?.apiCalls, store);
|
|
110
181
|
const timestamp = Date.now();
|
|
111
182
|
const startTime = process.hrtime.bigint();
|
|
112
183
|
// Emit NODE_STARTED
|
|
@@ -118,7 +189,7 @@ exports.evergreen = {
|
|
|
118
189
|
previousStep,
|
|
119
190
|
location,
|
|
120
191
|
children: options?.children || [],
|
|
121
|
-
apiCalls:
|
|
192
|
+
apiCalls: filteredApiCalls,
|
|
122
193
|
__not_async: options?.__not_async ?? false,
|
|
123
194
|
timestamp,
|
|
124
195
|
});
|
|
@@ -151,7 +222,7 @@ exports.evergreen = {
|
|
|
151
222
|
location,
|
|
152
223
|
// include children and apiCalls again so the UI can still render ghost edges & API badges even if NODE_STARTED was missed
|
|
153
224
|
children: options?.children || [],
|
|
154
|
-
apiCalls:
|
|
225
|
+
apiCalls: filteredApiCalls,
|
|
155
226
|
__not_async: options?.__not_async ?? false,
|
|
156
227
|
timestamp: Date.now(),
|
|
157
228
|
});
|
package/dist/index.test.d.ts
CHANGED
package/dist/index.test.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.testAlsPropagation = testAlsPropagation;
|
|
4
|
+
exports.testRunCallTraceFiltering = testRunCallTraceFiltering;
|
|
4
5
|
const index_1 = require("./index");
|
|
5
6
|
// Simple Node-style test (compatible with node --test once compiled)
|
|
6
7
|
// exported for use in dist/index.test.js
|
|
@@ -44,3 +45,39 @@ async function testAlsPropagation() {
|
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
}
|
|
48
|
+
async function testRunCallTraceFiltering() {
|
|
49
|
+
const events = [];
|
|
50
|
+
const originalSend = index_1.evergreen.__test__?.sendTelemetry;
|
|
51
|
+
if (originalSend) {
|
|
52
|
+
index_1.evergreen.__test__.sendTelemetry = (payload) => {
|
|
53
|
+
events.push(payload);
|
|
54
|
+
originalSend(payload);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
await index_1.evergreen.run("job-filter", { callTrace: { include: ["app.listen", "redis.*"], exclude: ["redis.ping"] } }, async () => {
|
|
59
|
+
await index_1.evergreen.__autoStep("step", { file: "test.ts", line: 1, column: 0 }, async () => 1, {
|
|
60
|
+
apiCalls: [
|
|
61
|
+
{ callee: "app.listen", arg0: "3000" },
|
|
62
|
+
{ callee: "redis.ping" },
|
|
63
|
+
{ callee: "redis.get", arg0: "k" },
|
|
64
|
+
{ callee: "prisma.user.findMany" },
|
|
65
|
+
],
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
const started = events.find((e) => e.event === "NODE_STARTED" && e.stepName === "step");
|
|
69
|
+
if (!started) {
|
|
70
|
+
throw new Error("Expected NODE_STARTED for step");
|
|
71
|
+
}
|
|
72
|
+
const callees = (started.apiCalls || []).map((c) => c.callee);
|
|
73
|
+
const expected = ["app.listen", "redis.get"];
|
|
74
|
+
if (JSON.stringify(callees) !== JSON.stringify(expected)) {
|
|
75
|
+
throw new Error(`Unexpected filtered callees: ${JSON.stringify(callees)} expected ${JSON.stringify(expected)}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
if (originalSend) {
|
|
80
|
+
index_1.evergreen.__test__.sendTelemetry = originalSend;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
package/package.json
CHANGED
package/src/index.test.ts
CHANGED
|
@@ -56,3 +56,56 @@ export async function testAlsPropagation() {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
export async function testRunCallTraceFiltering() {
|
|
60
|
+
const events: any[] = [];
|
|
61
|
+
const originalSend = (evergreen as any).__test__?.sendTelemetry;
|
|
62
|
+
|
|
63
|
+
if (originalSend) {
|
|
64
|
+
(evergreen as any).__test__.sendTelemetry = (payload: any) => {
|
|
65
|
+
events.push(payload);
|
|
66
|
+
originalSend(payload);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
await evergreen.run(
|
|
72
|
+
"job-filter",
|
|
73
|
+
{ callTrace: { include: ["app.listen", "redis.*"], exclude: ["redis.ping"] } },
|
|
74
|
+
async () => {
|
|
75
|
+
await evergreen.__autoStep(
|
|
76
|
+
"step",
|
|
77
|
+
{ file: "test.ts", line: 1, column: 0 },
|
|
78
|
+
async () => 1,
|
|
79
|
+
{
|
|
80
|
+
apiCalls: [
|
|
81
|
+
{ callee: "app.listen", arg0: "3000" },
|
|
82
|
+
{ callee: "redis.ping" },
|
|
83
|
+
{ callee: "redis.get", arg0: "k" },
|
|
84
|
+
{ callee: "prisma.user.findMany" },
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const started = events.find(
|
|
92
|
+
(e) => e.event === "NODE_STARTED" && e.stepName === "step",
|
|
93
|
+
);
|
|
94
|
+
if (!started) {
|
|
95
|
+
throw new Error("Expected NODE_STARTED for step");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const callees = (started.apiCalls || []).map((c: any) => c.callee);
|
|
99
|
+
const expected = ["app.listen", "redis.get"];
|
|
100
|
+
if (JSON.stringify(callees) !== JSON.stringify(expected)) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Unexpected filtered callees: ${JSON.stringify(callees)} expected ${JSON.stringify(expected)}`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
} finally {
|
|
106
|
+
if (originalSend) {
|
|
107
|
+
(evergreen as any).__test__.sendTelemetry = originalSend;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,10 @@ import WebSocket from "ws";
|
|
|
4
4
|
interface EvergreenContext {
|
|
5
5
|
jobId: string;
|
|
6
6
|
previousStep?: string;
|
|
7
|
+
callTrace?: {
|
|
8
|
+
include?: string[];
|
|
9
|
+
exclude?: string[];
|
|
10
|
+
};
|
|
7
11
|
}
|
|
8
12
|
|
|
9
13
|
interface ApiCallMeta {
|
|
@@ -11,6 +15,13 @@ interface ApiCallMeta {
|
|
|
11
15
|
arg0?: string;
|
|
12
16
|
}
|
|
13
17
|
|
|
18
|
+
interface RunOptions {
|
|
19
|
+
callTrace?: {
|
|
20
|
+
include?: string[]; // patterns like "app.listen" or "redis.*"
|
|
21
|
+
exclude?: string[];
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
interface StepOptions {
|
|
15
26
|
input?: any;
|
|
16
27
|
children?: string[];
|
|
@@ -36,8 +47,18 @@ function shouldDebugTelemetryConnection(): boolean {
|
|
|
36
47
|
);
|
|
37
48
|
}
|
|
38
49
|
|
|
50
|
+
function shouldDisableWs(): boolean {
|
|
51
|
+
// In tests, keeping a websocket instance around can keep the Node test runner alive.
|
|
52
|
+
if (process.env.NODE_ENV === "test") return true;
|
|
53
|
+
return (
|
|
54
|
+
process.env.EVERGREEN_TRACE_DISABLE_WS === "1" ||
|
|
55
|
+
process.env.EVERGREEN_TRACE_DISABLE_WS === "true"
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
39
59
|
// Connect to the VS Code Extension Hub
|
|
40
60
|
function connect() {
|
|
61
|
+
if (shouldDisableWs()) return;
|
|
41
62
|
if (
|
|
42
63
|
ws &&
|
|
43
64
|
(ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)
|
|
@@ -123,6 +144,48 @@ function sendTelemetry(payload: any) {
|
|
|
123
144
|
}
|
|
124
145
|
}
|
|
125
146
|
|
|
147
|
+
function escapeRegexLiteral(input: string): string {
|
|
148
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function matchesPattern(value: string, pattern: string): boolean {
|
|
152
|
+
// Support:
|
|
153
|
+
// - exact match: "app.listen"
|
|
154
|
+
// - simple wildcard: "redis.*" or "prisma.*.findMany" etc.
|
|
155
|
+
if (!pattern) return false;
|
|
156
|
+
if (!pattern.includes("*")) return value === pattern;
|
|
157
|
+
|
|
158
|
+
const re = new RegExp(
|
|
159
|
+
"^" +
|
|
160
|
+
pattern
|
|
161
|
+
.split("*")
|
|
162
|
+
.map((part) => escapeRegexLiteral(part))
|
|
163
|
+
.join(".*") +
|
|
164
|
+
"$",
|
|
165
|
+
);
|
|
166
|
+
return re.test(value);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function filterApiCallsForRun(
|
|
170
|
+
apiCalls: ApiCallMeta[] | undefined,
|
|
171
|
+
store: EvergreenContext,
|
|
172
|
+
): ApiCallMeta[] {
|
|
173
|
+
if (!apiCalls || apiCalls.length === 0) return [];
|
|
174
|
+
const include = store.callTrace?.include?.filter(Boolean) ?? [];
|
|
175
|
+
const exclude = store.callTrace?.exclude?.filter(Boolean) ?? [];
|
|
176
|
+
|
|
177
|
+
return apiCalls.filter((c) => {
|
|
178
|
+
if (exclude.length && exclude.some((p) => matchesPattern(c.callee, p))) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
if (include.length) {
|
|
182
|
+
return include.some((p) => matchesPattern(c.callee, p));
|
|
183
|
+
}
|
|
184
|
+
// default: include everything that was collected
|
|
185
|
+
return true;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
126
189
|
function extractLocationFromStack(
|
|
127
190
|
stack?: string,
|
|
128
191
|
): { file: string; line: number } | undefined {
|
|
@@ -146,14 +209,27 @@ function extractLocationFromStack(
|
|
|
146
209
|
return undefined;
|
|
147
210
|
}
|
|
148
211
|
|
|
212
|
+
function runWithContext<T>(jobId: string, fn: () => T): T;
|
|
213
|
+
function runWithContext<T>(jobId: string, options: RunOptions, fn: () => T): T;
|
|
214
|
+
function runWithContext<T>(
|
|
215
|
+
jobId: string,
|
|
216
|
+
a: RunOptions | (() => T),
|
|
217
|
+
b?: () => T,
|
|
218
|
+
): T {
|
|
219
|
+
const fn = typeof a === "function" ? a : b;
|
|
220
|
+
const options = typeof a === "function" ? undefined : a;
|
|
221
|
+
if (!fn) {
|
|
222
|
+
throw new Error("evergreen.run(jobId, fn) requires a function");
|
|
223
|
+
}
|
|
224
|
+
return traceStorage.run({ jobId, callTrace: options?.callTrace }, fn);
|
|
225
|
+
}
|
|
226
|
+
|
|
149
227
|
export const evergreen = {
|
|
150
228
|
clear() {
|
|
151
229
|
sendTelemetry({ type: "CLEAR" });
|
|
152
230
|
},
|
|
153
231
|
|
|
154
|
-
run
|
|
155
|
-
return traceStorage.run({ jobId }, fn);
|
|
156
|
-
},
|
|
232
|
+
run: runWithContext,
|
|
157
233
|
|
|
158
234
|
async __autoStep<T>(
|
|
159
235
|
name: string,
|
|
@@ -169,6 +245,7 @@ export const evergreen = {
|
|
|
169
245
|
}
|
|
170
246
|
|
|
171
247
|
const { jobId, previousStep } = store;
|
|
248
|
+
const filteredApiCalls = filterApiCallsForRun(options?.apiCalls, store);
|
|
172
249
|
|
|
173
250
|
const timestamp = Date.now();
|
|
174
251
|
const startTime = process.hrtime.bigint();
|
|
@@ -182,7 +259,7 @@ export const evergreen = {
|
|
|
182
259
|
previousStep,
|
|
183
260
|
location,
|
|
184
261
|
children: options?.children || [],
|
|
185
|
-
apiCalls:
|
|
262
|
+
apiCalls: filteredApiCalls,
|
|
186
263
|
__not_async: options?.__not_async ?? false,
|
|
187
264
|
timestamp,
|
|
188
265
|
});
|
|
@@ -219,7 +296,7 @@ export const evergreen = {
|
|
|
219
296
|
location,
|
|
220
297
|
// include children and apiCalls again so the UI can still render ghost edges & API badges even if NODE_STARTED was missed
|
|
221
298
|
children: options?.children || [],
|
|
222
|
-
apiCalls:
|
|
299
|
+
apiCalls: filteredApiCalls,
|
|
223
300
|
__not_async: options?.__not_async ?? false,
|
|
224
301
|
timestamp: Date.now(),
|
|
225
302
|
});
|
package/src/index.js
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.evergreen = void 0;
|
|
4
|
-
const node_async_hooks_1 = require("node:async_hooks");
|
|
5
|
-
const ws_1 = require("ws");
|
|
6
|
-
const traceStorage = new node_async_hooks_1.AsyncLocalStorage();
|
|
7
|
-
let ws = null;
|
|
8
|
-
let isConnected = false;
|
|
9
|
-
// Connect to the VS Code Extension Hub
|
|
10
|
-
function connect() {
|
|
11
|
-
if (ws && (ws.readyState === ws_1.default.CONNECTING || ws.readyState === ws_1.default.OPEN))
|
|
12
|
-
return;
|
|
13
|
-
try {
|
|
14
|
-
ws = new ws_1.default('ws://localhost:3000');
|
|
15
|
-
ws.on('open', () => {
|
|
16
|
-
isConnected = true;
|
|
17
|
-
});
|
|
18
|
-
ws.on('close', () => {
|
|
19
|
-
isConnected = false;
|
|
20
|
-
// Optionally implement reconnect logic here
|
|
21
|
-
});
|
|
22
|
-
ws.on('error', () => {
|
|
23
|
-
isConnected = false;
|
|
24
|
-
// Silently fail if extension is not running
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
catch (error) {
|
|
28
|
-
isConnected = false;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
// Ensure connection is attempted
|
|
32
|
-
connect();
|
|
33
|
-
function sanitizePayload(obj, maxDepth = 3) {
|
|
34
|
-
if (maxDepth <= 0)
|
|
35
|
-
return '[Max Depth Reached]';
|
|
36
|
-
if (obj === null || obj === undefined)
|
|
37
|
-
return obj;
|
|
38
|
-
if (typeof obj !== 'object')
|
|
39
|
-
return obj;
|
|
40
|
-
if (obj instanceof Error) {
|
|
41
|
-
return { name: obj.name, message: obj.message, stack: obj.stack };
|
|
42
|
-
}
|
|
43
|
-
if (Array.isArray(obj)) {
|
|
44
|
-
return obj.map(item => sanitizePayload(item, maxDepth - 1));
|
|
45
|
-
}
|
|
46
|
-
const sanitized = {};
|
|
47
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
48
|
-
sanitized[key] = sanitizePayload(value, maxDepth - 1);
|
|
49
|
-
}
|
|
50
|
-
return sanitized;
|
|
51
|
-
}
|
|
52
|
-
function sendTelemetry(payload) {
|
|
53
|
-
if (!isConnected || !ws || ws.readyState !== ws_1.default.OPEN)
|
|
54
|
-
return;
|
|
55
|
-
try {
|
|
56
|
-
ws.send(JSON.stringify(payload));
|
|
57
|
-
}
|
|
58
|
-
catch (error) {
|
|
59
|
-
// Fire and forget, silently ignore send errors
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
function extractLocationFromStack(stack) {
|
|
63
|
-
if (!stack)
|
|
64
|
-
return undefined;
|
|
65
|
-
// The stack will look like:
|
|
66
|
-
// Error
|
|
67
|
-
// at Object.step (/path/to/sdk/index.ts:...)
|
|
68
|
-
// at myBusinessLogic (/path/to/business/logic.ts:42:15)
|
|
69
|
-
const lines = stack.split('\n');
|
|
70
|
-
if (lines.length >= 3) {
|
|
71
|
-
const callerLine = lines[2];
|
|
72
|
-
const match = callerLine.match(/\((.+):(\d+):\d+\)/) || callerLine.match(/at (.+):(\d+):\d+/);
|
|
73
|
-
if (match && match.length >= 3) {
|
|
74
|
-
return { file: match[1], line: parseInt(match[2], 10) };
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return undefined;
|
|
78
|
-
}
|
|
79
|
-
exports.evergreen = {
|
|
80
|
-
clear() {
|
|
81
|
-
sendTelemetry({ type: 'CLEAR' });
|
|
82
|
-
},
|
|
83
|
-
run(jobId, fn) {
|
|
84
|
-
return traceStorage.run({ jobId }, fn);
|
|
85
|
-
},
|
|
86
|
-
async step(name, fn, options) {
|
|
87
|
-
const store = traceStorage.getStore();
|
|
88
|
-
// If no context, just execute the function normally without tracing
|
|
89
|
-
if (!store) {
|
|
90
|
-
return await fn();
|
|
91
|
-
}
|
|
92
|
-
const { jobId, previousStep } = store;
|
|
93
|
-
// Capture location using a throwaway error
|
|
94
|
-
const locationError = new Error();
|
|
95
|
-
const location = extractLocationFromStack(locationError.stack) || { file: 'unknown', line: 0 };
|
|
96
|
-
const timestamp = Date.now();
|
|
97
|
-
const startTime = process.hrtime.bigint();
|
|
98
|
-
// Emit NODE_STARTED
|
|
99
|
-
sendTelemetry({
|
|
100
|
-
event: 'NODE_STARTED',
|
|
101
|
-
status: 'PENDING',
|
|
102
|
-
jobId,
|
|
103
|
-
stepName: name,
|
|
104
|
-
previousStep,
|
|
105
|
-
location,
|
|
106
|
-
timestamp
|
|
107
|
-
});
|
|
108
|
-
// We must ensure subsequent nested steps know this step is their parent.
|
|
109
|
-
// To do this, we run the inner function in a nested context.
|
|
110
|
-
const newContext = { ...store, previousStep: name };
|
|
111
|
-
try {
|
|
112
|
-
// Execute the business logic within the new nested context
|
|
113
|
-
const result = await traceStorage.run(newContext, async () => {
|
|
114
|
-
return await fn();
|
|
115
|
-
});
|
|
116
|
-
const endTime = process.hrtime.bigint();
|
|
117
|
-
const durationMs = Number(endTime - startTime) / 1000000;
|
|
118
|
-
// Emit NODE_COMPLETED
|
|
119
|
-
sendTelemetry({
|
|
120
|
-
event: 'NODE_COMPLETED',
|
|
121
|
-
status: 'SUCCESS',
|
|
122
|
-
jobId,
|
|
123
|
-
stepName: name,
|
|
124
|
-
previousStep,
|
|
125
|
-
payload: { input: sanitizePayload(options?.input), output: sanitizePayload(result) },
|
|
126
|
-
context: {
|
|
127
|
-
input: sanitizePayload(options?.input),
|
|
128
|
-
output: sanitizePayload(result),
|
|
129
|
-
duration: durationMs
|
|
130
|
-
},
|
|
131
|
-
location,
|
|
132
|
-
timestamp: Date.now()
|
|
133
|
-
});
|
|
134
|
-
return result;
|
|
135
|
-
}
|
|
136
|
-
catch (error) {
|
|
137
|
-
const endTime = process.hrtime.bigint();
|
|
138
|
-
const durationMs = Number(endTime - startTime) / 1000000;
|
|
139
|
-
// Emit NODE_FAILED
|
|
140
|
-
sendTelemetry({
|
|
141
|
-
event: 'NODE_FAILED',
|
|
142
|
-
status: 'FAILED',
|
|
143
|
-
jobId,
|
|
144
|
-
stepName: name,
|
|
145
|
-
previousStep,
|
|
146
|
-
payload: { input: sanitizePayload(options?.input), error: sanitizePayload(error) },
|
|
147
|
-
context: {
|
|
148
|
-
input: sanitizePayload(options?.input),
|
|
149
|
-
error: sanitizePayload(error),
|
|
150
|
-
duration: durationMs
|
|
151
|
-
},
|
|
152
|
-
location,
|
|
153
|
-
timestamp: Date.now()
|
|
154
|
-
});
|
|
155
|
-
// Re-throw so backend logic behaves normally
|
|
156
|
-
throw error;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
//# sourceMappingURL=index.js.map
|