evergreen-sdk 1.0.2 → 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 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<T>(jobId: string, fn: () => T): T;
21
+ run: typeof runWithContext;
14
22
  __autoStep<T>(name: string, location: {
15
23
  file: string;
16
24
  line: number;
package/dist/index.js CHANGED
@@ -20,8 +20,17 @@ function shouldDebugTelemetryConnection() {
20
20
  return (process.env.EVERGREEN_TRACE_DEBUG === "1" ||
21
21
  process.env.EVERGREEN_TRACE_DEBUG === "true");
22
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
+ }
23
30
  // Connect to the VS Code Extension Hub
24
31
  function connect() {
32
+ if (shouldDisableWs())
33
+ return;
25
34
  if (ws &&
26
35
  (ws.readyState === ws_1.default.CONNECTING || ws.readyState === ws_1.default.OPEN))
27
36
  return;
@@ -95,6 +104,41 @@ function sendTelemetry(payload) {
95
104
  // Fire and forget, silently ignore send errors
96
105
  }
97
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
+ }
98
142
  function extractLocationFromStack(stack) {
99
143
  if (!stack)
100
144
  return undefined;
@@ -113,13 +157,19 @@ function extractLocationFromStack(stack) {
113
157
  }
114
158
  return undefined;
115
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
+ }
116
168
  exports.evergreen = {
117
169
  clear() {
118
170
  sendTelemetry({ type: "CLEAR" });
119
171
  },
120
- run(jobId, fn) {
121
- return traceStorage.run({ jobId }, fn);
122
- },
172
+ run: runWithContext,
123
173
  async __autoStep(name, location, fn, options) {
124
174
  const store = traceStorage.getStore();
125
175
  // If no context, just execute the function normally without tracing
@@ -127,6 +177,7 @@ exports.evergreen = {
127
177
  return await fn();
128
178
  }
129
179
  const { jobId, previousStep } = store;
180
+ const filteredApiCalls = filterApiCallsForRun(options?.apiCalls, store);
130
181
  const timestamp = Date.now();
131
182
  const startTime = process.hrtime.bigint();
132
183
  // Emit NODE_STARTED
@@ -138,7 +189,7 @@ exports.evergreen = {
138
189
  previousStep,
139
190
  location,
140
191
  children: options?.children || [],
141
- apiCalls: options?.apiCalls || [],
192
+ apiCalls: filteredApiCalls,
142
193
  __not_async: options?.__not_async ?? false,
143
194
  timestamp,
144
195
  });
@@ -171,7 +222,7 @@ exports.evergreen = {
171
222
  location,
172
223
  // include children and apiCalls again so the UI can still render ghost edges & API badges even if NODE_STARTED was missed
173
224
  children: options?.children || [],
174
- apiCalls: options?.apiCalls || [],
225
+ apiCalls: filteredApiCalls,
175
226
  __not_async: options?.__not_async ?? false,
176
227
  timestamp: Date.now(),
177
228
  });
@@ -1 +1,2 @@
1
1
  export declare function testAlsPropagation(): Promise<void>;
2
+ export declare function testRunCallTraceFiltering(): Promise<void>;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evergreen-sdk",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Evergreen Trace Backend SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
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<T>(jobId: string, fn: () => T): T {
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: options?.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: options?.apiCalls || [],
299
+ apiCalls: filteredApiCalls,
223
300
  __not_async: options?.__not_async ?? false,
224
301
  timestamp: Date.now(),
225
302
  });