@flareapp/core 2.2.0 → 2.2.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 +806 -0
- package/dist/index.d.cts +411 -0
- package/dist/index.d.mts +411 -0
- package/dist/index.mjs +760 -0
- package/package.json +4 -1
- package/.oxlintrc.json +0 -7
- package/.release-it.json +0 -13
- package/CHANGELOG.md +0 -16
- package/src/Flare.ts +0 -543
- package/src/Scope.ts +0 -96
- package/src/api/Api.ts +0 -35
- package/src/api/index.ts +0 -1
- package/src/env/index.ts +0 -14
- package/src/index.ts +0 -41
- package/src/stacktrace/NullFileReader.ts +0 -28
- package/src/stacktrace/createStackTrace.ts +0 -74
- package/src/stacktrace/fileReader.ts +0 -96
- package/src/stacktrace/index.ts +0 -4
- package/src/types.ts +0 -81
- package/src/util/assert.ts +0 -9
- package/src/util/assertKey.ts +0 -11
- package/src/util/convertToError.ts +0 -22
- package/src/util/extractCode.ts +0 -11
- package/src/util/flatJsonStringify.ts +0 -45
- package/src/util/glowsToEvents.ts +0 -16
- package/src/util/index.ts +0 -8
- package/src/util/now.ts +0 -3
- package/src/util/redactUrl.ts +0 -83
- package/tests/api.test.ts +0 -95
- package/tests/configure.test.ts +0 -16
- package/tests/contextCollector.test.ts +0 -37
- package/tests/convertToError.test.ts +0 -95
- package/tests/createStackTrace.test.ts +0 -54
- package/tests/extractCode.test.ts +0 -30
- package/tests/fileReader.test.ts +0 -51
- package/tests/flatJsonStringify.test.ts +0 -31
- package/tests/flush.test.ts +0 -47
- package/tests/glows.test.ts +0 -47
- package/tests/glowsToEvents.test.ts +0 -41
- package/tests/helpers/FakeApi.ts +0 -20
- package/tests/helpers/index.ts +0 -1
- package/tests/hooks.test.ts +0 -123
- package/tests/light.test.ts +0 -25
- package/tests/nullFileReader.test.ts +0 -11
- package/tests/publicExports.test.ts +0 -17
- package/tests/redactUrl.test.ts +0 -151
- package/tests/report.test.ts +0 -146
- package/tests/sampleRate.test.ts +0 -88
- package/tests/scope.test.ts +0 -64
- package/tests/setEntryPoint.test.ts +0 -79
- package/tests/setFramework.test.ts +0 -48
- package/tests/setSdkInfo.test.ts +0 -62
- package/tsconfig.json +0 -4
- package/vitest.config.ts +0 -17
package/tests/hooks.test.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { beforeEach, expect, test } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { Flare } from '../src';
|
|
4
|
-
import { FakeApi } from './helpers';
|
|
5
|
-
|
|
6
|
-
let fakeApi: FakeApi;
|
|
7
|
-
let client: Flare;
|
|
8
|
-
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
fakeApi = new FakeApi();
|
|
11
|
-
client = new Flare(fakeApi).configure({ key: 'key', debug: true });
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
test('beforeEvaluate returning null cancels the report', async () => {
|
|
15
|
-
client.configure({ beforeEvaluate: () => null });
|
|
16
|
-
await client.report(new Error());
|
|
17
|
-
expect(fakeApi.reports).toHaveLength(0);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
test('beforeEvaluate returning false cancels the report', async () => {
|
|
21
|
-
client.configure({ beforeEvaluate: () => false });
|
|
22
|
-
await client.report(new Error());
|
|
23
|
-
expect(fakeApi.reports).toHaveLength(0);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test('async beforeEvaluate returning null cancels the report', async () => {
|
|
27
|
-
client.configure({ beforeEvaluate: async () => null });
|
|
28
|
-
await client.report(new Error());
|
|
29
|
-
expect(fakeApi.reports).toHaveLength(0);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test('async beforeEvaluate returning false cancels the report', async () => {
|
|
33
|
-
client.configure({ beforeEvaluate: async (): Promise<false> => false });
|
|
34
|
-
await client.report(new Error());
|
|
35
|
-
expect(fakeApi.reports).toHaveLength(0);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test('beforeEvaluate can mutate the error', async () => {
|
|
39
|
-
client.configure({
|
|
40
|
-
beforeEvaluate: (error) => {
|
|
41
|
-
error.message = 'rewritten';
|
|
42
|
-
return error;
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
|
-
await client.report(new Error());
|
|
46
|
-
expect(fakeApi.lastReport?.message).toBe('rewritten');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('async beforeEvaluate can mutate the error', async () => {
|
|
50
|
-
client.configure({
|
|
51
|
-
beforeEvaluate: async (error) => {
|
|
52
|
-
error.message = 'rewritten';
|
|
53
|
-
return error;
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
await client.report(new Error());
|
|
57
|
-
expect(fakeApi.lastReport?.message).toBe('rewritten');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test('beforeSubmit returning null cancels the report', async () => {
|
|
61
|
-
client.configure({ beforeSubmit: () => null });
|
|
62
|
-
await client.report(new Error());
|
|
63
|
-
expect(fakeApi.reports).toHaveLength(0);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test('beforeSubmit returning false cancels the report', async () => {
|
|
67
|
-
client.configure({ beforeSubmit: () => false });
|
|
68
|
-
await client.report(new Error());
|
|
69
|
-
expect(fakeApi.reports).toHaveLength(0);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test('async beforeSubmit returning null cancels the report', async () => {
|
|
73
|
-
client.configure({ beforeSubmit: async () => null });
|
|
74
|
-
await client.report(new Error());
|
|
75
|
-
expect(fakeApi.reports).toHaveLength(0);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test('async beforeSubmit returning false cancels the report', async () => {
|
|
79
|
-
client.configure({ beforeSubmit: async (): Promise<false> => false });
|
|
80
|
-
await client.report(new Error());
|
|
81
|
-
expect(fakeApi.reports).toHaveLength(0);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test('beforeSubmit can mutate the report (camelCase fields)', async () => {
|
|
85
|
-
client.configure({
|
|
86
|
-
beforeSubmit: (report) => {
|
|
87
|
-
report.message = 'rewritten';
|
|
88
|
-
return report;
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
await client.report(new Error());
|
|
92
|
-
expect(fakeApi.lastReport?.message).toBe('rewritten');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test('async beforeSubmit can mutate the report', async () => {
|
|
96
|
-
client.configure({
|
|
97
|
-
beforeSubmit: async (report) => {
|
|
98
|
-
report.message = 'rewritten';
|
|
99
|
-
return report;
|
|
100
|
-
},
|
|
101
|
-
});
|
|
102
|
-
await client.report(new Error());
|
|
103
|
-
expect(fakeApi.lastReport?.message).toBe('rewritten');
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test('beforeEvaluate can replace the error with a new Error', async () => {
|
|
107
|
-
client.configure({
|
|
108
|
-
beforeEvaluate: () => new Error('replaced'),
|
|
109
|
-
});
|
|
110
|
-
await client.report(new Error('original'));
|
|
111
|
-
expect(fakeApi.lastReport?.message).toBe('replaced');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test('beforeSubmit can mutate attributes', async () => {
|
|
115
|
-
client.configure({
|
|
116
|
-
beforeSubmit: (report) => {
|
|
117
|
-
report.attributes['custom.tag'] = 'value';
|
|
118
|
-
return report;
|
|
119
|
-
},
|
|
120
|
-
});
|
|
121
|
-
await client.report(new Error());
|
|
122
|
-
expect(fakeApi.lastReport?.attributes['custom.tag']).toBe('value');
|
|
123
|
-
});
|
package/tests/light.test.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { expect, test } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { Flare } from '../src';
|
|
4
|
-
|
|
5
|
-
test('light', () => {
|
|
6
|
-
const client = new Flare();
|
|
7
|
-
client.light('key');
|
|
8
|
-
|
|
9
|
-
expect(client.config.key).toBe('key');
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
test('light() does not clobber a previously configured debug=true', () => {
|
|
13
|
-
const client = new Flare();
|
|
14
|
-
client.configure({ debug: true });
|
|
15
|
-
client.light('key');
|
|
16
|
-
|
|
17
|
-
expect(client.config.debug).toBe(true);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
test('light() can still explicitly opt into debug', () => {
|
|
21
|
-
const client = new Flare();
|
|
22
|
-
client.light('key', true);
|
|
23
|
-
|
|
24
|
-
expect(client.config.debug).toBe(true);
|
|
25
|
-
});
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { NullFileReader } from '../src/stacktrace/NullFileReader';
|
|
4
|
-
|
|
5
|
-
describe('NullFileReader', () => {
|
|
6
|
-
it('returns null for any URL', async () => {
|
|
7
|
-
const reader = new NullFileReader();
|
|
8
|
-
expect(await reader.read('https://example.com/foo.js')).toBeNull();
|
|
9
|
-
expect(await reader.read('/local/path.ts')).toBeNull();
|
|
10
|
-
});
|
|
11
|
-
});
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { createStackTrace, getCodeSnippet, readLinesFromFile } from '../src';
|
|
4
|
-
|
|
5
|
-
describe('public exports from @flareapp/core', () => {
|
|
6
|
-
it('exports createStackTrace as a function', () => {
|
|
7
|
-
expect(typeof createStackTrace).toBe('function');
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it('exports getCodeSnippet as a function', () => {
|
|
11
|
-
expect(typeof getCodeSnippet).toBe('function');
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('exports readLinesFromFile as a function', () => {
|
|
15
|
-
expect(typeof readLinesFromFile).toBe('function');
|
|
16
|
-
});
|
|
17
|
-
});
|
package/tests/redactUrl.test.ts
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_URL_DENYLIST, Flare, redactUrlQuery, resolveDenylist } from '@flareapp/core';
|
|
2
|
-
// @vitest-environment jsdom
|
|
3
|
-
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
4
|
-
|
|
5
|
-
import type { Attributes, Config } from '../src/types';
|
|
6
|
-
import { FakeApi } from './helpers/FakeApi';
|
|
7
|
-
|
|
8
|
-
describe('redactUrlQuery', () => {
|
|
9
|
-
test('redacts denylisted query keys', () => {
|
|
10
|
-
const result = redactUrlQuery('/page?token=abc&q=visible', DEFAULT_URL_DENYLIST);
|
|
11
|
-
expect(result).toBe('/page?token=[redacted]&q=visible');
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
test('redacts session-style keys', () => {
|
|
15
|
-
const result = redactUrlQuery('/page?session_id=xyz&tab=open', DEFAULT_URL_DENYLIST);
|
|
16
|
-
expect(result).toBe('/page?session_id=[redacted]&tab=open');
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test('preserves hash fragment after query', () => {
|
|
20
|
-
const result = redactUrlQuery('/page?token=abc#section', DEFAULT_URL_DENYLIST);
|
|
21
|
-
expect(result).toBe('/page?token=[redacted]#section');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test('handles hash-router URLs (query inside hash)', () => {
|
|
25
|
-
const url = 'http://localhost/#/users/77?token=secret&tab=open';
|
|
26
|
-
const result = redactUrlQuery(url, DEFAULT_URL_DENYLIST);
|
|
27
|
-
expect(result).toBe('http://localhost/#/users/77?token=[redacted]&tab=open');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test('returns input unchanged when no query string present', () => {
|
|
31
|
-
expect(redactUrlQuery('/page', DEFAULT_URL_DENYLIST)).toBe('/page');
|
|
32
|
-
expect(redactUrlQuery('', DEFAULT_URL_DENYLIST)).toBe('');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test('honours a custom denylist', () => {
|
|
36
|
-
const result = redactUrlQuery('/page?secretKey=xyz&token=visible', /^secretKey$/);
|
|
37
|
-
expect(result).toBe('/page?secretKey=[redacted]&token=visible');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test('redacts keys without values', () => {
|
|
41
|
-
expect(redactUrlQuery('/page?token', DEFAULT_URL_DENYLIST)).toBe('/page?token');
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
describe('resolveDenylist', () => {
|
|
46
|
-
test('returns default when no custom pattern given', () => {
|
|
47
|
-
expect(resolveDenylist()).toBe(DEFAULT_URL_DENYLIST);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test('merges custom pattern with default', () => {
|
|
51
|
-
const merged = resolveDenylist(/myParam/i);
|
|
52
|
-
expect(merged.test('password')).toBe(true);
|
|
53
|
-
expect(merged.test('myParam')).toBe(true);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test('replaces default when replaceDefault is true', () => {
|
|
57
|
-
const replaced = resolveDenylist(/myParam/i, true);
|
|
58
|
-
expect(replaced.test('password')).toBe(false);
|
|
59
|
-
expect(replaced.test('myParam')).toBe(true);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test('strips global/sticky flags to prevent stateful .test()', () => {
|
|
63
|
-
const resolved = resolveDenylist(/secret/gi, true);
|
|
64
|
-
expect(resolved.flags).not.toContain('g');
|
|
65
|
-
expect(resolved.flags).not.toContain('y');
|
|
66
|
-
|
|
67
|
-
const url = '/page?secret=1&secret=2';
|
|
68
|
-
const result = redactUrlQuery(url, resolved);
|
|
69
|
-
expect(result).toBe('/page?secret=[redacted]&secret=[redacted]');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test('strips global/sticky flags when merging with default', () => {
|
|
73
|
-
const resolved = resolveDenylist(/myParam/gy);
|
|
74
|
-
expect(resolved.flags).not.toContain('g');
|
|
75
|
-
expect(resolved.flags).not.toContain('y');
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
function browserCollector(config: Readonly<Config>): Attributes {
|
|
80
|
-
const attrs: Attributes = { 'flare.entry_point.type': 'web' };
|
|
81
|
-
if (typeof window !== 'undefined' && window?.location?.href) {
|
|
82
|
-
attrs['flare.entry_point.value'] = redactUrlQuery(window.location.href, config.urlDenylist);
|
|
83
|
-
attrs['url.full'] = redactUrlQuery(window.location.href, config.urlDenylist);
|
|
84
|
-
if (window.location.search) {
|
|
85
|
-
attrs['url.query'] = redactUrlQuery(window.location.search, config.urlDenylist).replace(/^\?/, '');
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return attrs;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
describe('Flare URL scrubbing', () => {
|
|
92
|
-
let flare: Flare;
|
|
93
|
-
let api: FakeApi;
|
|
94
|
-
let originalHref: string;
|
|
95
|
-
|
|
96
|
-
beforeEach(() => {
|
|
97
|
-
originalHref = window.location.href;
|
|
98
|
-
api = new FakeApi();
|
|
99
|
-
flare = new Flare(api, browserCollector);
|
|
100
|
-
flare.light('test-key', false);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
afterEach(() => {
|
|
104
|
-
window.history.replaceState({}, '', originalHref);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test('scrubs flare.entry_point.value, url.full, and url.query with default denylist', async () => {
|
|
108
|
-
window.history.replaceState({}, '', '/page?token=secret&q=visible');
|
|
109
|
-
|
|
110
|
-
await flare.report(new Error('boom'));
|
|
111
|
-
|
|
112
|
-
const attributes = api.lastReport!.attributes;
|
|
113
|
-
expect(attributes['flare.entry_point.value']).toContain('token=[redacted]');
|
|
114
|
-
expect(attributes['flare.entry_point.value']).toContain('q=visible');
|
|
115
|
-
expect(attributes['url.full']).toContain('token=[redacted]');
|
|
116
|
-
expect(attributes['url.query']).toBe('token=[redacted]&q=visible');
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test('merges custom urlDenylist with default', async () => {
|
|
120
|
-
window.history.replaceState({}, '', '/page?secretKey=xyz&token=abc&q=visible');
|
|
121
|
-
flare.configure({ urlDenylist: /^secretKey$/ });
|
|
122
|
-
|
|
123
|
-
await flare.report(new Error('boom'));
|
|
124
|
-
|
|
125
|
-
const attributes = api.lastReport!.attributes;
|
|
126
|
-
expect(attributes['url.full']).toContain('secretKey=[redacted]');
|
|
127
|
-
expect(attributes['url.full']).toContain('token=[redacted]');
|
|
128
|
-
expect(attributes['url.full']).toContain('q=visible');
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
test('replaces default urlDenylist when replaceDefaultUrlDenylist is true', async () => {
|
|
132
|
-
window.history.replaceState({}, '', '/page?secretKey=xyz&token=stillVisible');
|
|
133
|
-
flare.configure({ urlDenylist: /^secretKey$/, replaceDefaultUrlDenylist: true });
|
|
134
|
-
|
|
135
|
-
await flare.report(new Error('boom'));
|
|
136
|
-
|
|
137
|
-
const attributes = api.lastReport!.attributes;
|
|
138
|
-
expect(attributes['url.full']).toContain('secretKey=[redacted]');
|
|
139
|
-
expect(attributes['url.full']).toContain('token=stillVisible');
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test('omits url.query when no query string', async () => {
|
|
143
|
-
window.history.replaceState({}, '', '/page');
|
|
144
|
-
|
|
145
|
-
await flare.report(new Error('boom'));
|
|
146
|
-
|
|
147
|
-
const attributes = api.lastReport!.attributes;
|
|
148
|
-
expect(attributes['url.full']).toBeDefined();
|
|
149
|
-
expect(attributes['url.query']).toBeUndefined();
|
|
150
|
-
});
|
|
151
|
-
});
|
package/tests/report.test.ts
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { beforeEach, expect, test, vi } from 'vitest';
|
|
3
|
-
|
|
4
|
-
import { Flare } from '../src';
|
|
5
|
-
import { FakeApi } from './helpers';
|
|
6
|
-
|
|
7
|
-
let fakeApi: FakeApi;
|
|
8
|
-
let client: Flare;
|
|
9
|
-
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
fakeApi = new FakeApi();
|
|
12
|
-
client = new Flare(fakeApi).configure({ key: 'key', debug: true });
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
test('report() emits new-format payload with required attributes', async () => {
|
|
16
|
-
await client.report(new Error('boom'));
|
|
17
|
-
|
|
18
|
-
const r = fakeApi.lastReport!;
|
|
19
|
-
expect(r.message).toBe('boom');
|
|
20
|
-
expect(r.exceptionClass).toBe('Error');
|
|
21
|
-
expect(r.isLog).toBeUndefined();
|
|
22
|
-
expect(r.attributes['telemetry.sdk.language']).toBe('javascript');
|
|
23
|
-
expect(r.attributes['telemetry.sdk.name']).toBe('@flareapp/core');
|
|
24
|
-
expect(r.attributes['flare.language.name']).toBe('javascript');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test('report() emits flare.entry_point.type=web when browser collector is injected', async () => {
|
|
28
|
-
const browserClient = new Flare(fakeApi, () => ({ 'flare.entry_point.type': 'web' })).configure({
|
|
29
|
-
key: 'key',
|
|
30
|
-
debug: true,
|
|
31
|
-
});
|
|
32
|
-
await browserClient.report(new Error('boom'));
|
|
33
|
-
expect(fakeApi.lastReport!.attributes['flare.entry_point.type']).toBe('web');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test('report() seenAtUnixNano is roughly current time in nanoseconds', async () => {
|
|
37
|
-
const before = Date.now() * 1_000_000;
|
|
38
|
-
await client.report(new Error('x'));
|
|
39
|
-
const after = Date.now() * 1_000_000;
|
|
40
|
-
|
|
41
|
-
const seen = fakeApi.lastReport!.seenAtUnixNano;
|
|
42
|
-
// Allow ~1ms wiggle in either direction.
|
|
43
|
-
expect(seen).toBeGreaterThanOrEqual(before - 1_000_000);
|
|
44
|
-
expect(seen).toBeLessThanOrEqual(after + 1_000_000);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test('reportMessage() sets isLog true and exceptionClass=Log', async () => {
|
|
48
|
-
await client.reportMessage('Hello, Flare!');
|
|
49
|
-
|
|
50
|
-
expect(fakeApi.lastReport!.isLog).toBe(true);
|
|
51
|
-
expect(fakeApi.lastReport!.exceptionClass).toBe('Log');
|
|
52
|
-
expect(fakeApi.lastReport!.message).toBe('Hello, Flare!');
|
|
53
|
-
expect(fakeApi.lastReport!.level).toBeUndefined();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test('reportMessage() with explicit level emits level field', async () => {
|
|
57
|
-
await client.reportMessage('hi', 'warning');
|
|
58
|
-
|
|
59
|
-
expect(fakeApi.lastReport!.level).toBe('warning');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test('payload contains stacktrace with camelCase frames', async () => {
|
|
63
|
-
await client.report(new Error('x'));
|
|
64
|
-
|
|
65
|
-
const frame = fakeApi.lastReport!.stacktrace[0];
|
|
66
|
-
expect(frame).toHaveProperty('lineNumber');
|
|
67
|
-
expect(frame).toHaveProperty('file');
|
|
68
|
-
expect(frame).not.toHaveProperty('line_number');
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test('does not report browser extension errors by default', async () => {
|
|
72
|
-
await client.test();
|
|
73
|
-
expect(fakeApi.lastReportBrowserExtensionErrors).toBe(false);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test('can be configured to report browser extension errors', async () => {
|
|
77
|
-
client.configure({ reportBrowserExtensionErrors: true });
|
|
78
|
-
await client.test();
|
|
79
|
-
expect(fakeApi.lastReportBrowserExtensionErrors).toBe(true);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test('auto-populates code from error.code when string', async () => {
|
|
83
|
-
const err = Object.assign(new Error('boom'), { code: 'ENOTFOUND' });
|
|
84
|
-
await client.report(err);
|
|
85
|
-
|
|
86
|
-
expect(fakeApi.lastReport!.code).toBe('ENOTFOUND');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test('omits code when error.code is missing', async () => {
|
|
90
|
-
await client.report(new Error('boom'));
|
|
91
|
-
|
|
92
|
-
expect(fakeApi.lastReport!.code).toBeUndefined();
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test('addContext writes into attributes["context.custom"]', async () => {
|
|
96
|
-
client.addContext('userId', 7);
|
|
97
|
-
|
|
98
|
-
await client.report(new Error('x'));
|
|
99
|
-
|
|
100
|
-
expect(fakeApi.lastReport!.attributes['context.custom']).toEqual({ userId: 7 });
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test('addContextGroup writes into attributes["context.<group>"]', async () => {
|
|
104
|
-
client.addContextGroup('tenant', { id: 9 });
|
|
105
|
-
|
|
106
|
-
await client.report(new Error('x'));
|
|
107
|
-
|
|
108
|
-
expect(fakeApi.lastReport!.attributes['context.tenant']).toEqual({ id: 9 });
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test('per-call attributes win over collected attributes', async () => {
|
|
112
|
-
await client.report(new Error('x'), { 'telemetry.sdk.name': 'overridden' });
|
|
113
|
-
|
|
114
|
-
expect(fakeApi.lastReport!.attributes['telemetry.sdk.name']).toBe('overridden');
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test('events is always an array — never undefined', async () => {
|
|
118
|
-
await client.report(new Error('x'));
|
|
119
|
-
expect(Array.isArray(fakeApi.lastReport!.events)).toBe(true);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test('seenAtUnixNano reflects report() entry time, not post-stacktrace time', async () => {
|
|
123
|
-
vi.useFakeTimers();
|
|
124
|
-
vi.setSystemTime(new Date('2026-04-28T12:00:00.000Z'));
|
|
125
|
-
const entryNs = Date.now() * 1_000_000;
|
|
126
|
-
|
|
127
|
-
client.configure({
|
|
128
|
-
beforeEvaluate: (e) => {
|
|
129
|
-
vi.advanceTimersByTime(5000);
|
|
130
|
-
return e;
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
await client.report(new Error('x'));
|
|
135
|
-
|
|
136
|
-
expect(fakeApi.lastReport!.seenAtUnixNano).toBe(entryNs);
|
|
137
|
-
|
|
138
|
-
vi.useRealTimers();
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test('coerces non-Error inputs to a string message and Error class', async () => {
|
|
142
|
-
await client.report('plain string boom' as unknown as Error);
|
|
143
|
-
|
|
144
|
-
expect(fakeApi.lastReport!.message).toBe('plain string boom');
|
|
145
|
-
expect(fakeApi.lastReport!.exceptionClass).toBe('Error');
|
|
146
|
-
});
|
package/tests/sampleRate.test.ts
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
|
|
3
|
-
|
|
4
|
-
import { Flare } from '../src';
|
|
5
|
-
import { FakeApi } from './helpers';
|
|
6
|
-
|
|
7
|
-
let fakeApi: FakeApi;
|
|
8
|
-
let client: Flare;
|
|
9
|
-
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
fakeApi = new FakeApi();
|
|
12
|
-
client = new Flare(fakeApi).configure({ key: 'key', debug: true });
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
vi.restoreAllMocks();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test('sampleRate defaults to 1 (all errors reported)', async () => {
|
|
20
|
-
expect(client.config.sampleRate).toBe(1);
|
|
21
|
-
|
|
22
|
-
await client.report(new Error('boom'));
|
|
23
|
-
expect(fakeApi.lastReport).toBeDefined();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test('sampleRate 0 drops all errors', async () => {
|
|
27
|
-
client.configure({ sampleRate: 0 });
|
|
28
|
-
|
|
29
|
-
for (let i = 0; i < 20; i++) {
|
|
30
|
-
await client.report(new Error(`error-${i}`));
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
expect(fakeApi.reports).toHaveLength(0);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test('sampleRate 0 drops all messages', async () => {
|
|
37
|
-
client.configure({ sampleRate: 0 });
|
|
38
|
-
|
|
39
|
-
for (let i = 0; i < 20; i++) {
|
|
40
|
-
await client.reportMessage(`msg-${i}`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
expect(fakeApi.reports).toHaveLength(0);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test('sampleRate 1 reports all errors', async () => {
|
|
47
|
-
client.configure({ sampleRate: 1 });
|
|
48
|
-
|
|
49
|
-
for (let i = 0; i < 10; i++) {
|
|
50
|
-
await client.report(new Error(`error-${i}`));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
expect(fakeApi.reports).toHaveLength(10);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test('sampleRate controls proportion of reported errors', async () => {
|
|
57
|
-
client.configure({ sampleRate: 0.5 });
|
|
58
|
-
|
|
59
|
-
let callIndex = 0;
|
|
60
|
-
vi.spyOn(Math, 'random').mockImplementation(() => {
|
|
61
|
-
return callIndex++ % 2 === 0 ? 0.3 : 0.7;
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
for (let i = 0; i < 10; i++) {
|
|
65
|
-
await client.report(new Error(`error-${i}`));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
expect(fakeApi.reports).toHaveLength(5);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test('test() always sends regardless of sampleRate', async () => {
|
|
72
|
-
client.configure({ sampleRate: 0 });
|
|
73
|
-
|
|
74
|
-
await client.test();
|
|
75
|
-
|
|
76
|
-
expect(fakeApi.lastReport).toBeDefined();
|
|
77
|
-
expect(fakeApi.lastReport!.message).toBe('The Flare client is set up correctly!');
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test('sampleRate is clamped to 0 when negative', () => {
|
|
81
|
-
client.configure({ sampleRate: -0.5 });
|
|
82
|
-
expect(client.config.sampleRate).toBe(0);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test('sampleRate is clamped to 1 when above 1', () => {
|
|
86
|
-
client.configure({ sampleRate: 2 });
|
|
87
|
-
expect(client.config.sampleRate).toBe(1);
|
|
88
|
-
});
|
package/tests/scope.test.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { GlobalScopeProvider, Scope } from '../src/Scope';
|
|
4
|
-
import type { Glow } from '../src/types';
|
|
5
|
-
|
|
6
|
-
const glow = (name: string): Glow => ({
|
|
7
|
-
name,
|
|
8
|
-
messageLevel: 'info',
|
|
9
|
-
metaData: {},
|
|
10
|
-
time: 0,
|
|
11
|
-
microtime: 0,
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
describe('Scope', () => {
|
|
15
|
-
it('starts empty', () => {
|
|
16
|
-
const scope = new Scope();
|
|
17
|
-
expect(scope.glows).toEqual([]);
|
|
18
|
-
expect(scope.pendingAttributes).toEqual({});
|
|
19
|
-
expect(scope.entryPoint).toBeNull();
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('adds glows and caps at max', () => {
|
|
23
|
-
const scope = new Scope();
|
|
24
|
-
scope.addGlow(glow('a'), 2);
|
|
25
|
-
scope.addGlow(glow('b'), 2);
|
|
26
|
-
scope.addGlow(glow('c'), 2);
|
|
27
|
-
expect(scope.glows.map((g) => g.name)).toEqual(['b', 'c']);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('clears glows', () => {
|
|
31
|
-
const scope = new Scope();
|
|
32
|
-
scope.addGlow(glow('a'), 10);
|
|
33
|
-
scope.clearGlows();
|
|
34
|
-
expect(scope.glows).toEqual([]);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('sets and merges attributes', () => {
|
|
38
|
-
const scope = new Scope();
|
|
39
|
-
scope.setAttribute('foo', 'bar');
|
|
40
|
-
scope.mergeAttributes({ baz: 1, foo: 'baz' });
|
|
41
|
-
expect(scope.pendingAttributes).toEqual({ foo: 'baz', baz: 1 });
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('stores entryPoint', () => {
|
|
45
|
-
const scope = new Scope();
|
|
46
|
-
scope.entryPoint = { identifier: '/foo', type: 'browser' };
|
|
47
|
-
expect(scope.entryPoint?.identifier).toBe('/foo');
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe('GlobalScopeProvider', () => {
|
|
52
|
-
it('returns the same scope on every active() call', () => {
|
|
53
|
-
const provider = new GlobalScopeProvider();
|
|
54
|
-
const a = provider.active();
|
|
55
|
-
const b = provider.active();
|
|
56
|
-
expect(a).toBe(b);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('mutations on the active scope persist', () => {
|
|
60
|
-
const provider = new GlobalScopeProvider();
|
|
61
|
-
provider.active().setAttribute('k', 'v');
|
|
62
|
-
expect(provider.active().pendingAttributes).toEqual({ k: 'v' });
|
|
63
|
-
});
|
|
64
|
-
});
|