@forge-ops/tracker 0.11.0 → 0.12.0
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/README.md +50 -0
- package/package.json +1 -1
- package/src/changes.js +147 -0
- package/src/client.js +17 -0
- package/src/configuration.js +40 -0
- package/src/index.js +78 -2
package/README.md
CHANGED
|
@@ -375,6 +375,56 @@ flush and would otherwise grow it for as long as the process lives. A NaN or inf
|
|
|
375
375
|
dropped at capture: `JSON.stringify` turns it into `null` and the server would reject the whole
|
|
376
376
|
batch behind it. Requires a ForgeOps plan that includes custom metrics / infrastructure monitoring.
|
|
377
377
|
|
|
378
|
+
## What changed
|
|
379
|
+
|
|
380
|
+
ForgeOps can show what changed in your system next to the errors and slowdowns that followed it.
|
|
381
|
+
Two ways in:
|
|
382
|
+
|
|
383
|
+
**Record a change yourself** when something changes that no deploy captures, like a feature flag
|
|
384
|
+
flipped, a config value edited, or a migration run by hand:
|
|
385
|
+
|
|
386
|
+
```js
|
|
387
|
+
import * as forgeOpsTracker from "@forge-ops/tracker";
|
|
388
|
+
|
|
389
|
+
forgeOpsTracker.recordChange({
|
|
390
|
+
kind: "feature_flag", // feature_flag, config, migration, dependency, infrastructure, or other
|
|
391
|
+
title: "Enabled new_checkout for 10% of users",
|
|
392
|
+
details: { flag: "new_checkout", rolloutPercent: 10 },
|
|
393
|
+
actor: "ops@example.com",
|
|
394
|
+
url: "https://flags.example.com/new_checkout",
|
|
395
|
+
});
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
`kind` and `title` are required; `details`, `environment` (defaults to the configured one),
|
|
399
|
+
`service`, `actor`, `url`, `id` (an idempotency key, so sending the same change twice records it
|
|
400
|
+
once), and `occurredAt` (a `Date` or ISO 8601 string, defaulting to now) are optional. An unknown
|
|
401
|
+
`kind` is sent as `other`. It's queued and delivered on the same async loop as error events, so it
|
|
402
|
+
never slows down the caller, never throws, and is a no-op when the client isn't enabled for the
|
|
403
|
+
environment.
|
|
404
|
+
|
|
405
|
+
**Changes between deploys are detected for you.** Once per process, `init()` schedules a snapshot
|
|
406
|
+
of what the process is running: the Node version, and each of the `dependencies` in your app's
|
|
407
|
+
`package.json` (read from `appRoot`, the working directory by default) resolved to the version
|
|
408
|
+
actually installed in `node_modules`. ForgeOps compares it with the previous boot's and records
|
|
409
|
+
whatever changed, such as a package upgrade. It's sent on a later event-loop turn, so startup never
|
|
410
|
+
waits on it.
|
|
411
|
+
|
|
412
|
+
```js
|
|
413
|
+
forgeOpsTracker.init({
|
|
414
|
+
dsn: "https://<api_key>@getforgeops.net/api/v1/events",
|
|
415
|
+
detectChanges: true, // default; false sends no startup snapshot
|
|
416
|
+
trackEnvVarNames: false, // default; true also sends environment variable names
|
|
417
|
+
});
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
With `trackEnvVarNames` on, the snapshot lists the names of your environment variables (never their
|
|
421
|
+
values), so an added or removed variable shows up as a change. Names that differ from host to host,
|
|
422
|
+
like `HOSTNAME`, `PATH`, `PORT`, `LC_*`, and Kubernetes service variables, are left out, as are the
|
|
423
|
+
SDK's own `FORGE_OPS_*` settings.
|
|
424
|
+
|
|
425
|
+
Requires a ForgeOps plan that includes change tracking; on a plan that doesn't, both are rejected
|
|
426
|
+
server-side and dropped, exactly like any other delivery failure.
|
|
427
|
+
|
|
378
428
|
## Distributed tracing
|
|
379
429
|
|
|
380
430
|
For one slow or errored request, `forgeOpsTrackerTracingExpressMiddleware` (Express) and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forge-ops/tracker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "ForgeOps error tracking client: captures unhandled exceptions (Express/Fastify integration, plus explicit capture anywhere else) and delivers them to ForgeOps over HTTP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/changes.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Payloads for "what changed": one explicit recordChange() call (POST /api/v1/changes), and the
|
|
6
|
+
* startup snapshot (POST /api/v1/change_snapshots) that ForgeOps diffs against the previous boot's
|
|
7
|
+
* to record what changed between deploys. Mirrors gems/forge_ops_tracker's Change and
|
|
8
|
+
* ChangeSnapshot.
|
|
9
|
+
*
|
|
10
|
+
* The snapshot leaves out any key it can't determine reliably rather than guessing, since the
|
|
11
|
+
* server reads a missing key as "unknown", never as "everything was removed". Environment variable
|
|
12
|
+
* names are only ever names, never values, and only when trackEnvVarNames is on.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The server rejects any other kind outright (422), so an unknown one is sent as "other" rather
|
|
16
|
+
* than dropped: the change still gets recorded, just less specifically categorized. */
|
|
17
|
+
export const CHANGE_KINDS = Object.freeze(["feature_flag", "config", "migration", "dependency", "infrastructure", "other"]);
|
|
18
|
+
const MAX_TITLE_LENGTH = 200;
|
|
19
|
+
|
|
20
|
+
const MAX_DEPENDENCIES = 3000;
|
|
21
|
+
const MAX_NAME_LENGTH = 200;
|
|
22
|
+
const MAX_VERSION_LENGTH = 100;
|
|
23
|
+
const MAX_ENV_VAR_NAMES = 3000;
|
|
24
|
+
|
|
25
|
+
/** Host-specific variables that differ from one box to the next (or one boot to the next) without
|
|
26
|
+
* anything about the deploy having changed, so a fleet doesn't look like it's changing on every
|
|
27
|
+
* restart. The SDK's own FORGE_OPS_* settings are dropped too. */
|
|
28
|
+
const ENV_VAR_DENYLIST = new Set([
|
|
29
|
+
"HOSTNAME", "HOST", "HOME", "PATH", "PWD", "OLDPWD", "SHLVL", "_", "TERM", "USER", "LOGNAME", "SHELL",
|
|
30
|
+
"LANG", "TMPDIR", "TZ", "PORT", "DYNO", "INVOCATION_ID", "JOURNAL_STREAM",
|
|
31
|
+
]);
|
|
32
|
+
const ENV_VAR_DENYLIST_PATTERNS = [
|
|
33
|
+
/^LC_/, /^SYSTEMD_/, /^MEMORY_PRESSURE_/, /^KUBERNETES_/, /^FORGE_OPS_/,
|
|
34
|
+
/_SERVICE_HOST$/, /_SERVICE_PORT/, /_PORT_.*_TCP/,
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/** @param {unknown} kind */
|
|
38
|
+
export function normalizeKind(kind) {
|
|
39
|
+
return CHANGE_KINDS.includes(kind) ? kind : "other";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The body for one recordChange() call, or null when there's nothing sendable (a blank title: the
|
|
44
|
+
* server requires one, so posting it anyway would only ever come back 422).
|
|
45
|
+
* @param {import("./configuration.js").Configuration} configuration
|
|
46
|
+
* @param {{ kind: string, title: string, details?: Record<string, unknown>, environment?: string,
|
|
47
|
+
* service?: string, actor?: string, url?: string, id?: string, occurredAt?: Date | string }} change
|
|
48
|
+
*/
|
|
49
|
+
export function buildChange(configuration, { kind, title, details, environment, service, actor, url, id, occurredAt } = {}) {
|
|
50
|
+
const trimmed = title == null ? "" : String(title).trim();
|
|
51
|
+
if (trimmed === "") {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const payload = {
|
|
56
|
+
kind: normalizeKind(kind),
|
|
57
|
+
title: trimmed.slice(0, MAX_TITLE_LENGTH),
|
|
58
|
+
details: details !== null && typeof details === "object" && !Array.isArray(details) ? details : {},
|
|
59
|
+
environment: String(environment ?? configuration.environment),
|
|
60
|
+
};
|
|
61
|
+
for (const [key, value] of Object.entries({ service, actor, url, id })) {
|
|
62
|
+
if (value != null) {
|
|
63
|
+
payload[key] = String(value);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
payload.occurred_at = iso8601(occurredAt);
|
|
67
|
+
return payload;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @param {Date | string | undefined} value */
|
|
71
|
+
function iso8601(value) {
|
|
72
|
+
if (typeof value === "string") {
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
|
76
|
+
return value.toISOString();
|
|
77
|
+
}
|
|
78
|
+
return new Date().toISOString();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** @param {string} name */
|
|
82
|
+
export function isDeniedEnvVar(name) {
|
|
83
|
+
return ENV_VAR_DENYLIST.has(name) || ENV_VAR_DENYLIST_PATTERNS.some((pattern) => pattern.test(name));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Names only, never values, sorted so the same set always serializes the same way.
|
|
88
|
+
* @param {Record<string, string | undefined>} [env]
|
|
89
|
+
*/
|
|
90
|
+
export function envVarNames(env = process.env) {
|
|
91
|
+
return Object.keys(env)
|
|
92
|
+
.filter((name) => !isDeniedEnvVar(name))
|
|
93
|
+
.sort()
|
|
94
|
+
.slice(0, MAX_ENV_VAR_NAMES);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function runtime() {
|
|
98
|
+
return `node ${process.versions.node}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The app's own direct dependencies (the "dependencies" in `<appRoot>/package.json`), each
|
|
103
|
+
* resolved to the version actually installed under `<appRoot>/node_modules/<name>/package.json`,
|
|
104
|
+
* never the semver range the manifest asks for. A dependency that isn't installed there is left
|
|
105
|
+
* out rather than guessed at, and so is the whole key (null) when there's no package.json to read.
|
|
106
|
+
* @param {string} appRoot
|
|
107
|
+
*/
|
|
108
|
+
export async function dependencies(appRoot) {
|
|
109
|
+
let manifest;
|
|
110
|
+
try {
|
|
111
|
+
manifest = JSON.parse(await readFile(path.join(appRoot, "package.json"), "utf8"));
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const names = Object.keys(manifest?.dependencies ?? {}).sort().slice(0, MAX_DEPENDENCIES);
|
|
117
|
+
const resolved = await Promise.all(names.map((name) => installedVersion(appRoot, name)));
|
|
118
|
+
const found = {};
|
|
119
|
+
names.forEach((name, index) => {
|
|
120
|
+
if (resolved[index]) {
|
|
121
|
+
found[name.slice(0, MAX_NAME_LENGTH)] = resolved[index].slice(0, MAX_VERSION_LENGTH);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
return Object.keys(found).length > 0 ? found : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function installedVersion(appRoot, name) {
|
|
128
|
+
try {
|
|
129
|
+
const installed = JSON.parse(await readFile(path.join(appRoot, "node_modules", name, "package.json"), "utf8"));
|
|
130
|
+
return typeof installed?.version === "string" ? installed.version : null;
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** @param {import("./configuration.js").Configuration} configuration */
|
|
137
|
+
export async function buildSnapshot(configuration) {
|
|
138
|
+
const state = { runtime: runtime() };
|
|
139
|
+
const deps = await dependencies(configuration.appRoot);
|
|
140
|
+
if (deps) {
|
|
141
|
+
state.dependencies = deps;
|
|
142
|
+
}
|
|
143
|
+
if (configuration.trackEnvVarNames) {
|
|
144
|
+
state.env_var_names = envVarNames();
|
|
145
|
+
}
|
|
146
|
+
return { environment: configuration.environment, state };
|
|
147
|
+
}
|
package/src/client.js
CHANGED
|
@@ -68,6 +68,23 @@ export class Client {
|
|
|
68
68
|
return this.#post(this.#configuration.infrastructureMetricsUri(), { metrics });
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* One recordChange() call; see Api::V1::ChangesController. A plan without change tracking
|
|
73
|
+
* answers 403, which is just a false here like any other non-2xx.
|
|
74
|
+
* @param {Record<string, unknown>} payload
|
|
75
|
+
*/
|
|
76
|
+
async deliverChange(payload) {
|
|
77
|
+
return this.#post(this.#configuration.changesUri(), payload);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The startup snapshot; see Api::V1::ChangeSnapshotsController.
|
|
82
|
+
* @param {Record<string, unknown>} payload
|
|
83
|
+
*/
|
|
84
|
+
async deliverChangeSnapshot(payload) {
|
|
85
|
+
return this.#post(this.#configuration.changeSnapshotsUri(), payload);
|
|
86
|
+
}
|
|
87
|
+
|
|
71
88
|
/**
|
|
72
89
|
* @param {string | null} uri
|
|
73
90
|
* @param {Record<string, unknown>} payload
|
package/src/configuration.js
CHANGED
|
@@ -136,6 +136,21 @@ export class Configuration {
|
|
|
136
136
|
*/
|
|
137
137
|
tracePropagationTargets = null;
|
|
138
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Sends one startup snapshot per process (the Node version and the installed versions of your
|
|
141
|
+
* package.json dependencies; see changes.js) which ForgeOps diffs against the previous boot's to
|
|
142
|
+
* record what changed between deploys. On by default, same posture as every other automatic
|
|
143
|
+
* behavior here.
|
|
144
|
+
*/
|
|
145
|
+
detectChanges = true;
|
|
146
|
+
/**
|
|
147
|
+
* Whether that snapshot also lists the names of this process's environment variables, so an
|
|
148
|
+
* added or removed variable shows up as a change. Off by default: names only, never values, but
|
|
149
|
+
* even names can say more about an app than some teams want to share. Host-specific names
|
|
150
|
+
* (HOSTNAME, PATH, PORT, and so on; see changes.js) are always left out.
|
|
151
|
+
*/
|
|
152
|
+
trackEnvVarNames = false;
|
|
153
|
+
|
|
139
154
|
/** @returns {string | null} */
|
|
140
155
|
apiKey() {
|
|
141
156
|
const parsed = this.#parsedDsn();
|
|
@@ -223,6 +238,31 @@ export class Configuration {
|
|
|
223
238
|
return uri.replace(/\/events$/, "/spans");
|
|
224
239
|
}
|
|
225
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Same derivation again, swapping the trailing /events for /changes (recordChange()).
|
|
243
|
+
* @returns {string | null}
|
|
244
|
+
*/
|
|
245
|
+
changesUri() {
|
|
246
|
+
const uri = this.ingestionUri();
|
|
247
|
+
if (!uri) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
return uri.replace(/\/events$/, "/changes");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Same derivation again, swapping the trailing /events for /change_snapshots (the startup
|
|
255
|
+
* snapshot).
|
|
256
|
+
* @returns {string | null}
|
|
257
|
+
*/
|
|
258
|
+
changeSnapshotsUri() {
|
|
259
|
+
const uri = this.ingestionUri();
|
|
260
|
+
if (!uri) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
return uri.replace(/\/events$/, "/change_snapshots");
|
|
264
|
+
}
|
|
265
|
+
|
|
226
266
|
/**
|
|
227
267
|
* Whether an outbound request to `host` should carry a traceparent header; see
|
|
228
268
|
* propagateTraces/tracePropagationTargets above. Case-insensitive, since hostnames are.
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { BreadcrumbBuffer } from "./breadcrumbBuffer.js";
|
|
3
|
+
import { buildChange, buildSnapshot } from "./changes.js";
|
|
3
4
|
import { Client } from "./client.js";
|
|
4
5
|
import { Configuration } from "./configuration.js";
|
|
5
6
|
import { DeliveryQueue } from "./deliveryQueue.js";
|
|
@@ -21,6 +22,8 @@ let reporter = null;
|
|
|
21
22
|
let sessionFlusher = null;
|
|
22
23
|
let performanceFlusher = null;
|
|
23
24
|
let spanQueue = null;
|
|
25
|
+
let changeQueue = null;
|
|
26
|
+
let changeSnapshotStarted = false;
|
|
24
27
|
let metricBuffer = null;
|
|
25
28
|
let infrastructureMetricBuffer = null;
|
|
26
29
|
let processHandlersInstalled = false;
|
|
@@ -183,6 +186,70 @@ export async function flushMetrics() {
|
|
|
183
186
|
await Promise.all([metricBuffer.flush(), infrastructureMetricBuffer.flush()]);
|
|
184
187
|
}
|
|
185
188
|
|
|
189
|
+
function getChangeQueue() {
|
|
190
|
+
if (changeQueue === null) {
|
|
191
|
+
const config = getConfiguration();
|
|
192
|
+
changeQueue = new DeliveryQueue(config, new Client(config), { deliverMethod: "deliverChange", label: "change" });
|
|
193
|
+
}
|
|
194
|
+
return changeQueue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Records one thing that changed in a running system, so ForgeOps can show it next to the errors
|
|
199
|
+
* and slowdowns that followed:
|
|
200
|
+
*
|
|
201
|
+
* recordChange({ kind: "feature_flag", title: "Enabled new_checkout for 10%", details: { rollout: 10 } });
|
|
202
|
+
*
|
|
203
|
+
* `kind` is one of feature_flag, config, migration, dependency, infrastructure, or other (anything
|
|
204
|
+
* else is sent as "other"). `environment` defaults to the configured one; `occurredAt` (a Date or an
|
|
205
|
+
* ISO 8601 string) defaults to now; `id` is an optional idempotency key. Queued and delivered on the
|
|
206
|
+
* same async loop as error events, so it never blocks the caller. A no-op when the client isn't
|
|
207
|
+
* enabled. Never throws: returns false when nothing was queued.
|
|
208
|
+
*
|
|
209
|
+
* @param {{ kind: string, title: string, details?: Record<string, unknown>, environment?: string,
|
|
210
|
+
* service?: string, actor?: string, url?: string, id?: string, occurredAt?: Date | string }} change
|
|
211
|
+
* @returns {boolean}
|
|
212
|
+
*/
|
|
213
|
+
export function recordChange(change) {
|
|
214
|
+
try {
|
|
215
|
+
const config = getConfiguration();
|
|
216
|
+
if (!config.isEnabled()) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
const payload = buildChange(config, change ?? {});
|
|
220
|
+
if (payload === null) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
return getChangeQueue().push(payload);
|
|
224
|
+
} catch (e) {
|
|
225
|
+
getConfiguration().log(`[forge-ops-tracker] recordChange failed: ${e.name}: ${e.message}`);
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Sends the one startup snapshot this process sends (see changes.js), on a later event-loop turn so
|
|
232
|
+
* init() returns immediately and startup never waits on it. A second call is a no-op. Does nothing,
|
|
233
|
+
* and doesn't use up the once, when the client isn't enabled or detectChanges is off. The timer is
|
|
234
|
+
* unref'd, so a short script that's otherwise done doesn't stay alive just to send it. Never throws.
|
|
235
|
+
*/
|
|
236
|
+
function startChangeSnapshot(config) {
|
|
237
|
+
if (changeSnapshotStarted || !config.isEnabled() || !config.detectChanges) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
changeSnapshotStarted = true;
|
|
241
|
+
|
|
242
|
+
const timer = setTimeout(async () => {
|
|
243
|
+
try {
|
|
244
|
+
await new Client(config).deliverChangeSnapshot(await buildSnapshot(config));
|
|
245
|
+
} catch (e) {
|
|
246
|
+
config.log(`[forge-ops-tracker] change snapshot failed: ${e.name}: ${e.message}`);
|
|
247
|
+
}
|
|
248
|
+
}, 0);
|
|
249
|
+
timer.unref?.();
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
|
|
186
253
|
/**
|
|
187
254
|
* A DeliveryQueue reused for spans (see that class's own comment for why it takes a
|
|
188
255
|
* deliverMethod/label rather than needing a whole second, near-identical class the way
|
|
@@ -442,6 +509,8 @@ export function init(options = {}) {
|
|
|
442
509
|
// _recordSpan above), never at install time.
|
|
443
510
|
installHttpTracing(_recordSpan, _traceparentFor);
|
|
444
511
|
|
|
512
|
+
startChangeSnapshot(config);
|
|
513
|
+
|
|
445
514
|
return config;
|
|
446
515
|
}
|
|
447
516
|
|
|
@@ -621,8 +690,13 @@ function installProcessLevelHandlers() {
|
|
|
621
690
|
process.on("unhandledRejection", unhandledRejectionListener);
|
|
622
691
|
}
|
|
623
692
|
|
|
624
|
-
/**
|
|
625
|
-
|
|
693
|
+
/**
|
|
694
|
+
* @internal not part of the public API: resets module state between test cases. The startup change
|
|
695
|
+
* snapshot is left marked as already sent, so the many tests that init() an enabled client and
|
|
696
|
+
* count fetch calls don't see an extra one; pass { changeSnapshot: true } to let the next init()
|
|
697
|
+
* send it (see test/changes.test.js).
|
|
698
|
+
*/
|
|
699
|
+
export function _resetForTesting({ changeSnapshot = false } = {}) {
|
|
626
700
|
if (uncaughtExceptionListener) {
|
|
627
701
|
process.off("uncaughtExceptionMonitor", uncaughtExceptionListener);
|
|
628
702
|
}
|
|
@@ -634,6 +708,8 @@ export function _resetForTesting() {
|
|
|
634
708
|
sessionFlusher = null;
|
|
635
709
|
performanceFlusher = null;
|
|
636
710
|
spanQueue = null;
|
|
711
|
+
changeQueue = null;
|
|
712
|
+
changeSnapshotStarted = !changeSnapshot;
|
|
637
713
|
metricBuffer?.discard();
|
|
638
714
|
infrastructureMetricBuffer?.discard();
|
|
639
715
|
metricBuffer = null;
|