@flareapp/node 0.1.0 → 0.1.1
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.cjs +834 -0
- package/dist/index.d.cts +138 -0
- package/dist/index.d.mts +138 -0
- package/dist/index.mjs +755 -0
- package/package.json +5 -2
- package/.oxlintrc.json +0 -7
- package/.release-it.json +0 -13
- package/CHANGELOG.md +0 -22
- package/src/Flare.ts +0 -224
- package/src/context/body.ts +0 -185
- package/src/context/collectNode.ts +0 -116
- package/src/context/headers.ts +0 -90
- package/src/context/process.ts +0 -25
- package/src/index.ts +0 -27
- package/src/process/fatal.ts +0 -54
- package/src/process/handlers.ts +0 -109
- package/src/scope/AsyncLocalStorageScopeProvider.ts +0 -86
- package/src/scope/NodeScope.ts +0 -8
- package/src/stacktrace/DiskFileReader.ts +0 -57
- package/src/types.ts +0 -37
- package/tests/asyncScopeProvider.test.ts +0 -43
- package/tests/body.test.ts +0 -129
- package/tests/diskFileReader.test.ts +0 -36
- package/tests/fatalHandlers.test.ts +0 -140
- package/tests/flush.test.ts +0 -11
- package/tests/headers.test.ts +0 -86
- package/tests/integration.test.ts +0 -63
- package/tests/lifecycle.test.ts +0 -71
- package/tests/nodeContextCollector.test.ts +0 -106
- package/tests/nodeExports.test.ts +0 -11
- package/tests/nodeScope.test.ts +0 -19
- package/tests/processAttributes.test.ts +0 -15
- package/tests/processHandlers.test.ts +0 -47
- package/tests/regexFlagSanitization.test.ts +0 -88
- package/tests/scopeIsolation.test.ts +0 -47
- package/tests/setFrameworkScope.test.ts +0 -86
- package/tsconfig.json +0 -9
- package/vitest.config.ts +0 -18
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { writeFileSync, mkdtempSync } from 'node:fs';
|
|
2
|
-
import { tmpdir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { pathToFileURL } from 'node:url';
|
|
5
|
-
|
|
6
|
-
import { describe, expect, it } from 'vitest';
|
|
7
|
-
|
|
8
|
-
import { DiskFileReader } from '../src/stacktrace/DiskFileReader';
|
|
9
|
-
|
|
10
|
-
describe('DiskFileReader', () => {
|
|
11
|
-
it('reads a real file from disk by absolute path', async () => {
|
|
12
|
-
const dir = mkdtempSync(join(tmpdir(), 'flare-test-'));
|
|
13
|
-
const file = join(dir, 'sample.js');
|
|
14
|
-
writeFileSync(file, 'hello\nworld');
|
|
15
|
-
const reader = new DiskFileReader();
|
|
16
|
-
expect(await reader.read(file)).toBe('hello\nworld');
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('reads a file:// URL', async () => {
|
|
20
|
-
const dir = mkdtempSync(join(tmpdir(), 'flare-test-'));
|
|
21
|
-
const file = join(dir, 'sample.js');
|
|
22
|
-
writeFileSync(file, 'x');
|
|
23
|
-
const reader = new DiskFileReader();
|
|
24
|
-
expect(await reader.read(pathToFileURL(file).href)).toBe('x');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('returns null for http urls', async () => {
|
|
28
|
-
const reader = new DiskFileReader();
|
|
29
|
-
expect(await reader.read('https://example.com/x.js')).toBeNull();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('returns null for missing files', async () => {
|
|
33
|
-
const reader = new DiskFileReader();
|
|
34
|
-
expect(await reader.read('/definitely/not/a/file.js')).toBeNull();
|
|
35
|
-
});
|
|
36
|
-
});
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import { Flare, Api } from '@flareapp/core';
|
|
2
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
|
|
4
|
-
import { buildFatalCallbacks } from '../src/process/fatal';
|
|
5
|
-
|
|
6
|
-
function fakeFlare(): { flare: Flare; sent: any[] } {
|
|
7
|
-
const sent: any[] = [];
|
|
8
|
-
const api = new Api();
|
|
9
|
-
api.report = (report: any) => {
|
|
10
|
-
sent.push(report);
|
|
11
|
-
return Promise.resolve();
|
|
12
|
-
};
|
|
13
|
-
const flare = new Flare(api);
|
|
14
|
-
flare.light('k');
|
|
15
|
-
return { flare, sent };
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
afterEach(() => {
|
|
19
|
-
// Reset exitCode so one test doesn't bleed into the next
|
|
20
|
-
process.exitCode = undefined;
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
describe('fatal callbacks', () => {
|
|
24
|
-
it('uncaught: awaits full report pipeline before resolving', async () => {
|
|
25
|
-
const { flare, sent } = fakeFlare();
|
|
26
|
-
const exit = vi.fn();
|
|
27
|
-
const { onUncaught } = buildFatalCallbacks(
|
|
28
|
-
flare,
|
|
29
|
-
() => ({
|
|
30
|
-
uncaughtExceptionMode: 'report-and-exit',
|
|
31
|
-
unhandledRejectionMode: 'off',
|
|
32
|
-
shutdownTimeoutMs: 1000,
|
|
33
|
-
}),
|
|
34
|
-
exit,
|
|
35
|
-
);
|
|
36
|
-
await onUncaught(new Error('boom'), 'uncaughtException');
|
|
37
|
-
expect(sent.length).toBe(1);
|
|
38
|
-
expect(sent[0].attributes['process.uncaught_exception.origin']).toBe('uncaughtException');
|
|
39
|
-
expect(exit).toHaveBeenCalledWith(1);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('uncaught: does NOT exit when mode is report', async () => {
|
|
43
|
-
const { flare, sent } = fakeFlare();
|
|
44
|
-
const exit = vi.fn();
|
|
45
|
-
const { onUncaught } = buildFatalCallbacks(
|
|
46
|
-
flare,
|
|
47
|
-
() => ({
|
|
48
|
-
uncaughtExceptionMode: 'report',
|
|
49
|
-
unhandledRejectionMode: 'off',
|
|
50
|
-
shutdownTimeoutMs: 1000,
|
|
51
|
-
}),
|
|
52
|
-
exit,
|
|
53
|
-
);
|
|
54
|
-
await onUncaught(new Error('boom'), 'uncaughtException');
|
|
55
|
-
expect(sent.length).toBe(1);
|
|
56
|
-
expect(exit).not.toHaveBeenCalled();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('unhandled rejection: coerces non-Error reason', async () => {
|
|
60
|
-
const { flare, sent } = fakeFlare();
|
|
61
|
-
const exit = vi.fn();
|
|
62
|
-
const { onRejection } = buildFatalCallbacks(
|
|
63
|
-
flare,
|
|
64
|
-
() => ({
|
|
65
|
-
uncaughtExceptionMode: 'off',
|
|
66
|
-
unhandledRejectionMode: 'report',
|
|
67
|
-
shutdownTimeoutMs: 1000,
|
|
68
|
-
}),
|
|
69
|
-
exit,
|
|
70
|
-
);
|
|
71
|
-
await onRejection('a string reason');
|
|
72
|
-
expect(sent.length).toBe(1);
|
|
73
|
-
expect(sent[0].message).toBe('a string reason');
|
|
74
|
-
expect(exit).not.toHaveBeenCalled();
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('uncaught: sets process.exitCode=1 synchronously before awaiting report', async () => {
|
|
78
|
-
const { flare } = fakeFlare();
|
|
79
|
-
let exitCodeDuringReport: number | string | undefined;
|
|
80
|
-
const api = new Api();
|
|
81
|
-
api.report = () => {
|
|
82
|
-
exitCodeDuringReport = process.exitCode;
|
|
83
|
-
return Promise.resolve();
|
|
84
|
-
};
|
|
85
|
-
flare.api = api;
|
|
86
|
-
|
|
87
|
-
const exit = vi.fn();
|
|
88
|
-
const { onUncaught } = buildFatalCallbacks(
|
|
89
|
-
flare,
|
|
90
|
-
() => ({
|
|
91
|
-
uncaughtExceptionMode: 'report-and-exit',
|
|
92
|
-
unhandledRejectionMode: 'off',
|
|
93
|
-
shutdownTimeoutMs: 1000,
|
|
94
|
-
}),
|
|
95
|
-
exit,
|
|
96
|
-
);
|
|
97
|
-
await onUncaught(new Error('boom'), 'uncaughtException');
|
|
98
|
-
expect(exitCodeDuringReport).toBe(1);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('rejection: sets process.exitCode=1 synchronously before awaiting report', async () => {
|
|
102
|
-
const { flare } = fakeFlare();
|
|
103
|
-
let exitCodeDuringReport: number | string | undefined;
|
|
104
|
-
const api = new Api();
|
|
105
|
-
api.report = () => {
|
|
106
|
-
exitCodeDuringReport = process.exitCode;
|
|
107
|
-
return Promise.resolve();
|
|
108
|
-
};
|
|
109
|
-
flare.api = api;
|
|
110
|
-
|
|
111
|
-
const exit = vi.fn();
|
|
112
|
-
const { onRejection } = buildFatalCallbacks(
|
|
113
|
-
flare,
|
|
114
|
-
() => ({
|
|
115
|
-
uncaughtExceptionMode: 'off',
|
|
116
|
-
unhandledRejectionMode: 'report-and-exit',
|
|
117
|
-
shutdownTimeoutMs: 1000,
|
|
118
|
-
}),
|
|
119
|
-
exit,
|
|
120
|
-
);
|
|
121
|
-
await onRejection(new Error('rejected'));
|
|
122
|
-
expect(exitCodeDuringReport).toBe(1);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('uncaught: does NOT set process.exitCode when mode is report', async () => {
|
|
126
|
-
const { flare } = fakeFlare();
|
|
127
|
-
const exit = vi.fn();
|
|
128
|
-
const { onUncaught } = buildFatalCallbacks(
|
|
129
|
-
flare,
|
|
130
|
-
() => ({
|
|
131
|
-
uncaughtExceptionMode: 'report',
|
|
132
|
-
unhandledRejectionMode: 'off',
|
|
133
|
-
shutdownTimeoutMs: 1000,
|
|
134
|
-
}),
|
|
135
|
-
exit,
|
|
136
|
-
);
|
|
137
|
-
await onUncaught(new Error('boom'), 'uncaughtException');
|
|
138
|
-
expect(process.exitCode).toBeUndefined();
|
|
139
|
-
});
|
|
140
|
-
});
|
package/tests/flush.test.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { flare } from '../src';
|
|
4
|
-
|
|
5
|
-
describe('Node flare.flush', () => {
|
|
6
|
-
it('resolves quickly when there is nothing in flight', async () => {
|
|
7
|
-
const start = Date.now();
|
|
8
|
-
await flare.flush(1000);
|
|
9
|
-
expect(Date.now() - start).toBeLessThan(50);
|
|
10
|
-
});
|
|
11
|
-
});
|
package/tests/headers.test.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { DEFAULT_HEADER_DENYLIST, findHeader, projectHeaders } from '../src/context/headers';
|
|
4
|
-
|
|
5
|
-
describe('projectHeaders', () => {
|
|
6
|
-
it('emits each header as http.request.header.<lowercase-name>', () => {
|
|
7
|
-
const attrs = projectHeaders(
|
|
8
|
-
{ 'Content-Type': 'application/json', 'X-Foo': 'bar' },
|
|
9
|
-
{
|
|
10
|
-
headerDenylist: DEFAULT_HEADER_DENYLIST,
|
|
11
|
-
headerAllowlist: null,
|
|
12
|
-
},
|
|
13
|
-
);
|
|
14
|
-
expect(attrs['http.request.header.content-type']).toBe('application/json');
|
|
15
|
-
expect(attrs['http.request.header.x-foo']).toBe('bar');
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('redacts default-denylisted headers', () => {
|
|
19
|
-
const attrs = projectHeaders(
|
|
20
|
-
{ Authorization: 'Bearer xyz', Cookie: 'sid=1' },
|
|
21
|
-
{
|
|
22
|
-
headerDenylist: DEFAULT_HEADER_DENYLIST,
|
|
23
|
-
headerAllowlist: null,
|
|
24
|
-
},
|
|
25
|
-
);
|
|
26
|
-
expect(attrs['http.request.header.authorization']).toBe('[redacted]');
|
|
27
|
-
expect(attrs['http.request.header.cookie']).toBe('[redacted]');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('allowlist filters out non-allowed headers entirely', () => {
|
|
31
|
-
const attrs = projectHeaders(
|
|
32
|
-
{ 'X-Foo': 'bar', 'X-Baz': 'qux' },
|
|
33
|
-
{
|
|
34
|
-
headerDenylist: DEFAULT_HEADER_DENYLIST,
|
|
35
|
-
headerAllowlist: /^x-foo$/i,
|
|
36
|
-
},
|
|
37
|
-
);
|
|
38
|
-
expect(attrs['http.request.header.x-foo']).toBe('bar');
|
|
39
|
-
expect(attrs['http.request.header.x-baz']).toBeUndefined();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('coalesces array values', () => {
|
|
43
|
-
const attrs = projectHeaders(
|
|
44
|
-
{ 'X-Foo': ['a', 'b'] as any },
|
|
45
|
-
{
|
|
46
|
-
headerDenylist: DEFAULT_HEADER_DENYLIST,
|
|
47
|
-
headerAllowlist: null,
|
|
48
|
-
},
|
|
49
|
-
);
|
|
50
|
-
expect(attrs['http.request.header.x-foo']).toBe('a, b');
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe('findHeader', () => {
|
|
55
|
-
it('returns undefined when headers is undefined', () => {
|
|
56
|
-
expect(findHeader(undefined, 'content-type')).toBeUndefined();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('finds header with exact lowercase key', () => {
|
|
60
|
-
expect(findHeader({ 'content-type': 'application/json' }, 'content-type')).toBe('application/json');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('finds header with uppercase key', () => {
|
|
64
|
-
expect(findHeader({ 'CONTENT-TYPE': 'application/json' }, 'content-type')).toBe('application/json');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('finds header with mixed case key', () => {
|
|
68
|
-
expect(findHeader({ 'Content-type': 'application/json; charset=utf-8' }, 'content-type')).toBe(
|
|
69
|
-
'application/json; charset=utf-8',
|
|
70
|
-
);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('coalesces array values to first element', () => {
|
|
74
|
-
expect(findHeader({ 'content-type': ['application/json', 'text/plain'] as any }, 'content-type')).toBe(
|
|
75
|
-
'application/json',
|
|
76
|
-
);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('returns undefined when header is not present', () => {
|
|
80
|
-
expect(findHeader({ 'x-foo': 'bar' }, 'content-type')).toBeUndefined();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('returns undefined when header value is undefined', () => {
|
|
84
|
-
expect(findHeader({ 'content-type': undefined }, 'content-type')).toBeUndefined();
|
|
85
|
-
});
|
|
86
|
-
});
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { createServer, type Server } from 'node:http';
|
|
2
|
-
|
|
3
|
-
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
4
|
-
|
|
5
|
-
import { startFakeFlareServer } from '../../../e2e/fake-flare-server';
|
|
6
|
-
import type { FakeFlareServer } from '../../../e2e/fake-flare-server';
|
|
7
|
-
|
|
8
|
-
let fakeFlareServer: FakeFlareServer;
|
|
9
|
-
|
|
10
|
-
beforeAll(async () => {
|
|
11
|
-
fakeFlareServer = await startFakeFlareServer();
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
afterAll(async () => {
|
|
15
|
-
await fakeFlareServer?.stop();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
describe('Node SDK integration', () => {
|
|
19
|
-
it('reports an error through a real HTTP server', async () => {
|
|
20
|
-
const { flare } = await import('../src');
|
|
21
|
-
flare.removeProcessListeners();
|
|
22
|
-
flare.configureNode({ uncaughtExceptionMode: 'off', unhandledRejectionMode: 'off' });
|
|
23
|
-
flare.configure({ ingestUrl: `${fakeFlareServer.url}/api/reports` });
|
|
24
|
-
flare.light('test-key');
|
|
25
|
-
|
|
26
|
-
fakeFlareServer.reset();
|
|
27
|
-
|
|
28
|
-
let captured = false;
|
|
29
|
-
const app: Server = createServer((req, res) => {
|
|
30
|
-
flare.runWithContext({ method: req.method!, path: req.url! }, async () => {
|
|
31
|
-
if (req.url === '/boom') {
|
|
32
|
-
try {
|
|
33
|
-
throw new Error('integration-boom');
|
|
34
|
-
} catch (e) {
|
|
35
|
-
await flare.report(e as Error);
|
|
36
|
-
captured = true;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
res.end('ok');
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
await new Promise<void>((r) => app.listen(0, r));
|
|
44
|
-
const port = (app.address() as { port: number }).port;
|
|
45
|
-
|
|
46
|
-
await fetch(`http://localhost:${port}/boom`);
|
|
47
|
-
await flare.flush(1500);
|
|
48
|
-
|
|
49
|
-
expect(captured).toBe(true);
|
|
50
|
-
|
|
51
|
-
const reports = fakeFlareServer.reports();
|
|
52
|
-
expect(reports.length).toBe(1);
|
|
53
|
-
|
|
54
|
-
const body = reports[0].bodyJson as Record<string, unknown>;
|
|
55
|
-
expect(body.message).toBe('integration-boom');
|
|
56
|
-
|
|
57
|
-
const attributes = body.attributes as Record<string, unknown>;
|
|
58
|
-
expect(attributes['http.request.method']).toBe('GET');
|
|
59
|
-
expect(attributes['url.path']).toBe('/boom');
|
|
60
|
-
|
|
61
|
-
await new Promise<void>((r) => app.close(() => r()));
|
|
62
|
-
});
|
|
63
|
-
});
|
package/tests/lifecycle.test.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { flare } from '../src';
|
|
4
|
-
import { NodeFlare } from '../src/Flare';
|
|
5
|
-
|
|
6
|
-
describe('Node singleton lifecycle', () => {
|
|
7
|
-
it('configureNode before light: no listeners until light', () => {
|
|
8
|
-
flare.removeProcessListeners();
|
|
9
|
-
const before = process.listeners('uncaughtException').length;
|
|
10
|
-
flare.configureNode({ uncaughtExceptionMode: 'report' });
|
|
11
|
-
expect(process.listeners('uncaughtException').length).toBe(before); // not lit yet
|
|
12
|
-
flare.light('k');
|
|
13
|
-
expect(process.listeners('uncaughtException').length).toBe(before + 1);
|
|
14
|
-
flare.removeProcessListeners();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it('configureNode after light: dynamically attaches and detaches', () => {
|
|
18
|
-
flare.removeProcessListeners();
|
|
19
|
-
flare.configureNode({ uncaughtExceptionMode: 'off' });
|
|
20
|
-
flare.light('k');
|
|
21
|
-
const baseline = process.listeners('uncaughtException').length;
|
|
22
|
-
flare.configureNode({ uncaughtExceptionMode: 'report' });
|
|
23
|
-
expect(process.listeners('uncaughtException').length).toBe(baseline + 1);
|
|
24
|
-
flare.configureNode({ uncaughtExceptionMode: 'off' });
|
|
25
|
-
expect(process.listeners('uncaughtException').length).toBe(baseline);
|
|
26
|
-
flare.removeProcessListeners();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('runWithContext isolates request scope', () => {
|
|
30
|
-
const seen: Array<string | undefined> = [];
|
|
31
|
-
flare.runWithContext({ path: '/a' }, () => {
|
|
32
|
-
seen.push(flare.getContext()?.request.path);
|
|
33
|
-
});
|
|
34
|
-
flare.runWithContext({ path: '/b' }, () => {
|
|
35
|
-
seen.push(flare.getContext()?.request.path);
|
|
36
|
-
});
|
|
37
|
-
expect(flare.getContext()).toBeNull();
|
|
38
|
-
expect(seen).toEqual(['/a', '/b']);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('removeProcessListeners then light reattaches handlers', () => {
|
|
42
|
-
const instance = new NodeFlare();
|
|
43
|
-
instance.removeProcessListeners();
|
|
44
|
-
instance.configureNode({ uncaughtExceptionMode: 'report' });
|
|
45
|
-
instance.light('k');
|
|
46
|
-
const after_first_light = process.listeners('uncaughtException').length;
|
|
47
|
-
expect(after_first_light).toBeGreaterThanOrEqual(1);
|
|
48
|
-
const baseline = after_first_light - 1;
|
|
49
|
-
instance.removeProcessListeners();
|
|
50
|
-
expect(process.listeners('uncaughtException').length).toBe(baseline);
|
|
51
|
-
instance.light('k');
|
|
52
|
-
expect(process.listeners('uncaughtException').length).toBe(baseline + 1);
|
|
53
|
-
instance.removeProcessListeners();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('supports subclass fluent chaining', () => {
|
|
57
|
-
const instance = new NodeFlare();
|
|
58
|
-
// Type-only check: ensure subclass methods remain chainable
|
|
59
|
-
instance.configure({ stage: 'prod' }).configureNode({ uncaughtExceptionMode: 'off' });
|
|
60
|
-
instance.removeProcessListeners();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('configureNode({ headerAllowlist: null }) clears a previously set allowlist', () => {
|
|
64
|
-
const instance = new NodeFlare();
|
|
65
|
-
instance.configureNode({ headerAllowlist: /^x-foo$/i });
|
|
66
|
-
expect((instance as any).nodeOptions.headerAllowlist).not.toBeNull();
|
|
67
|
-
instance.configureNode({ headerAllowlist: null });
|
|
68
|
-
expect((instance as any).nodeOptions.headerAllowlist).toBeNull();
|
|
69
|
-
instance.removeProcessListeners();
|
|
70
|
-
});
|
|
71
|
-
});
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_URL_DENYLIST } from '@flareapp/core';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
|
|
4
|
-
import { DEFAULT_BODY_CONTENT_TYPES, DEFAULT_BODY_KEY_DENYLIST } from '../src/context/body';
|
|
5
|
-
import { makeNodeContextCollector } from '../src/context/collectNode';
|
|
6
|
-
import { DEFAULT_HEADER_DENYLIST } from '../src/context/headers';
|
|
7
|
-
import { AsyncLocalStorageScopeProvider } from '../src/scope/AsyncLocalStorageScopeProvider';
|
|
8
|
-
|
|
9
|
-
const baseOpts = {
|
|
10
|
-
headerDenylist: DEFAULT_HEADER_DENYLIST,
|
|
11
|
-
headerAllowlist: null,
|
|
12
|
-
captureRequestBody: false,
|
|
13
|
-
bodyAllowedContentTypes: DEFAULT_BODY_CONTENT_TYPES,
|
|
14
|
-
bodyKeyDenylist: DEFAULT_BODY_KEY_DENYLIST,
|
|
15
|
-
bodyMaxBytes: 16_384,
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
describe('Node ContextCollector', () => {
|
|
19
|
-
it('emits process attributes when called outside a scope', () => {
|
|
20
|
-
const provider = new AsyncLocalStorageScopeProvider();
|
|
21
|
-
const collect = makeNodeContextCollector(provider, () => baseOpts);
|
|
22
|
-
const attrs = collect({ urlDenylist: DEFAULT_URL_DENYLIST } as any);
|
|
23
|
-
expect(attrs['process.runtime.name']).toBe('nodejs');
|
|
24
|
-
expect(attrs['flare.entry_point.type']).toBe('server');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('projects request.path into url.path + url.query', () => {
|
|
28
|
-
const provider = new AsyncLocalStorageScopeProvider();
|
|
29
|
-
const collect = makeNodeContextCollector(provider, () => baseOpts);
|
|
30
|
-
provider.runWithContext({ method: 'POST', path: '/foo?bar=1&token=x' }, () => {
|
|
31
|
-
const attrs = collect({ urlDenylist: DEFAULT_URL_DENYLIST } as any);
|
|
32
|
-
expect(attrs['http.request.method']).toBe('POST');
|
|
33
|
-
expect(attrs['url.path']).toBe('/foo');
|
|
34
|
-
expect(attrs['url.query']).toBe('bar=1&token=[redacted]');
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('projects request.url through redactUrlQuery into url.full', () => {
|
|
39
|
-
const provider = new AsyncLocalStorageScopeProvider();
|
|
40
|
-
const collect = makeNodeContextCollector(provider, () => baseOpts);
|
|
41
|
-
provider.runWithContext({ url: 'https://x.test/a?password=hunter2' }, () => {
|
|
42
|
-
const attrs = collect({ urlDenylist: DEFAULT_URL_DENYLIST } as any);
|
|
43
|
-
expect(attrs['url.full']).toBe('https://x.test/a?password=[redacted]');
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('projects user fields with OTel keys', () => {
|
|
48
|
-
const provider = new AsyncLocalStorageScopeProvider();
|
|
49
|
-
const collect = makeNodeContextCollector(provider, () => baseOpts);
|
|
50
|
-
provider.runWithContext({}, () => {
|
|
51
|
-
provider.setUser({ id: 'u1', email: 'a@b.c', ipAddress: '1.2.3.4' });
|
|
52
|
-
const attrs = collect({ urlDenylist: DEFAULT_URL_DENYLIST } as any);
|
|
53
|
-
expect(attrs['enduser.id']).toBe('u1');
|
|
54
|
-
expect(attrs['enduser.email']).toBe('a@b.c');
|
|
55
|
-
expect(attrs['client.address']).toBe('1.2.3.4');
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('respects captureRequestBody=false', () => {
|
|
60
|
-
const provider = new AsyncLocalStorageScopeProvider();
|
|
61
|
-
const collect = makeNodeContextCollector(provider, () => baseOpts);
|
|
62
|
-
provider.runWithContext({ body: { a: 1 }, headers: { 'content-type': 'application/json' } }, () => {
|
|
63
|
-
const attrs = collect({ urlDenylist: DEFAULT_URL_DENYLIST } as any);
|
|
64
|
-
expect(attrs['http.request.body']).toBeUndefined();
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('captures body when enabled', () => {
|
|
69
|
-
const provider = new AsyncLocalStorageScopeProvider();
|
|
70
|
-
const collect = makeNodeContextCollector(provider, () => ({ ...baseOpts, captureRequestBody: true }));
|
|
71
|
-
provider.runWithContext({ body: { a: 1 }, headers: { 'content-type': 'application/json' } }, () => {
|
|
72
|
-
const attrs = collect({ urlDenylist: DEFAULT_URL_DENYLIST } as any);
|
|
73
|
-
expect(attrs['http.request.body']).toBe('{"a":1}');
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('captures body with CONTENT-TYPE header casing', () => {
|
|
78
|
-
const provider = new AsyncLocalStorageScopeProvider();
|
|
79
|
-
const collect = makeNodeContextCollector(provider, () => ({ ...baseOpts, captureRequestBody: true }));
|
|
80
|
-
provider.runWithContext({ body: { a: 1 }, headers: { 'CONTENT-TYPE': 'application/json' } }, () => {
|
|
81
|
-
const attrs = collect({ urlDenylist: DEFAULT_URL_DENYLIST } as any);
|
|
82
|
-
expect(attrs['http.request.body']).toBe('{"a":1}');
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('captures body with Content-type header casing', () => {
|
|
87
|
-
const provider = new AsyncLocalStorageScopeProvider();
|
|
88
|
-
const collect = makeNodeContextCollector(provider, () => ({ ...baseOpts, captureRequestBody: true }));
|
|
89
|
-
provider.runWithContext(
|
|
90
|
-
{ body: '{"a":1}', headers: { 'Content-type': 'application/json; charset=utf-8' } },
|
|
91
|
-
() => {
|
|
92
|
-
const attrs = collect({ urlDenylist: DEFAULT_URL_DENYLIST } as any);
|
|
93
|
-
expect(attrs['http.request.body']).toBe('{"a":1}');
|
|
94
|
-
},
|
|
95
|
-
);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('captures body when content-type is an array value (uses first element)', () => {
|
|
99
|
-
const provider = new AsyncLocalStorageScopeProvider();
|
|
100
|
-
const collect = makeNodeContextCollector(provider, () => ({ ...baseOpts, captureRequestBody: true }));
|
|
101
|
-
provider.runWithContext({ body: { a: 1 }, headers: { 'content-type': ['application/json'] as any } }, () => {
|
|
102
|
-
const attrs = collect({ urlDenylist: DEFAULT_URL_DENYLIST } as any);
|
|
103
|
-
expect(attrs['http.request.body']).toBe('{"a":1}');
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
});
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { NodeScope } from '../src';
|
|
4
|
-
|
|
5
|
-
describe('public exports from @flareapp/node', () => {
|
|
6
|
-
it('exports NodeScope as a class', () => {
|
|
7
|
-
expect(typeof NodeScope).toBe('function');
|
|
8
|
-
const s = new NodeScope();
|
|
9
|
-
expect(s).toBeDefined();
|
|
10
|
-
});
|
|
11
|
-
});
|
package/tests/nodeScope.test.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { NodeScope } from '../src/scope/NodeScope';
|
|
4
|
-
|
|
5
|
-
describe('NodeScope', () => {
|
|
6
|
-
it('starts with empty request and null user', () => {
|
|
7
|
-
const scope = new NodeScope();
|
|
8
|
-
expect(scope.request).toEqual({});
|
|
9
|
-
expect(scope.user).toBeNull();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('inherits core Scope behavior (glows, attributes, entryPoint)', () => {
|
|
13
|
-
const scope = new NodeScope();
|
|
14
|
-
scope.setAttribute('k', 'v');
|
|
15
|
-
scope.addGlow({ name: 'g', messageLevel: 'info', metaData: {}, time: 0, microtime: 0 }, 10);
|
|
16
|
-
expect(scope.pendingAttributes).toEqual({ k: 'v' });
|
|
17
|
-
expect(scope.glows.length).toBe(1);
|
|
18
|
-
});
|
|
19
|
-
});
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { collectProcessAttributes } from '../src/context/process';
|
|
4
|
-
|
|
5
|
-
describe('collectProcessAttributes', () => {
|
|
6
|
-
it('includes runtime + host attributes', () => {
|
|
7
|
-
const attrs = collectProcessAttributes();
|
|
8
|
-
expect(attrs['process.runtime.name']).toBe('nodejs');
|
|
9
|
-
expect(attrs['process.runtime.version']).toBe(process.version);
|
|
10
|
-
expect(typeof attrs['process.pid']).toBe('number');
|
|
11
|
-
expect(typeof attrs['process.uptime']).toBe('number');
|
|
12
|
-
expect(typeof attrs['host.name']).toBe('string');
|
|
13
|
-
expect(typeof attrs['os.type']).toBe('string');
|
|
14
|
-
});
|
|
15
|
-
});
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { ProcessHandlerManager } from '../src/process/handlers';
|
|
4
|
-
|
|
5
|
-
describe('ProcessHandlerManager', () => {
|
|
6
|
-
let manager: ProcessHandlerManager;
|
|
7
|
-
|
|
8
|
-
afterEach(() => {
|
|
9
|
-
manager?.detach();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('attaches no listeners when modes are off', () => {
|
|
13
|
-
const before = process.listeners('uncaughtException').length;
|
|
14
|
-
const beforeR = process.listeners('unhandledRejection').length;
|
|
15
|
-
manager = new ProcessHandlerManager({
|
|
16
|
-
onUncaught: vi.fn(),
|
|
17
|
-
onRejection: vi.fn(),
|
|
18
|
-
});
|
|
19
|
-
manager.reconcile({ uncaughtExceptionMode: 'off', unhandledRejectionMode: 'off' });
|
|
20
|
-
expect(process.listeners('uncaughtException').length).toBe(before);
|
|
21
|
-
expect(process.listeners('unhandledRejection').length).toBe(beforeR);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('attaches handlers when modes are report or report-and-exit', () => {
|
|
25
|
-
const before = process.listeners('uncaughtException').length;
|
|
26
|
-
manager = new ProcessHandlerManager({ onUncaught: vi.fn(), onRejection: vi.fn() });
|
|
27
|
-
manager.reconcile({ uncaughtExceptionMode: 'report', unhandledRejectionMode: 'report' });
|
|
28
|
-
expect(process.listeners('uncaughtException').length).toBe(before + 1);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('detaches when mode flips back to off', () => {
|
|
32
|
-
const before = process.listeners('uncaughtException').length;
|
|
33
|
-
manager = new ProcessHandlerManager({ onUncaught: vi.fn(), onRejection: vi.fn() });
|
|
34
|
-
manager.reconcile({ uncaughtExceptionMode: 'report', unhandledRejectionMode: 'off' });
|
|
35
|
-
expect(process.listeners('uncaughtException').length).toBe(before + 1);
|
|
36
|
-
manager.reconcile({ uncaughtExceptionMode: 'off', unhandledRejectionMode: 'off' });
|
|
37
|
-
expect(process.listeners('uncaughtException').length).toBe(before);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('is idempotent — does not attach twice on repeated reconcile', () => {
|
|
41
|
-
const before = process.listeners('uncaughtException').length;
|
|
42
|
-
manager = new ProcessHandlerManager({ onUncaught: vi.fn(), onRejection: vi.fn() });
|
|
43
|
-
manager.reconcile({ uncaughtExceptionMode: 'report', unhandledRejectionMode: 'off' });
|
|
44
|
-
manager.reconcile({ uncaughtExceptionMode: 'report', unhandledRejectionMode: 'off' });
|
|
45
|
-
expect(process.listeners('uncaughtException').length).toBe(before + 1);
|
|
46
|
-
});
|
|
47
|
-
});
|