@flareapp/core 2.2.0 → 2.3.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.
Files changed (55) hide show
  1. package/README.md +4 -1
  2. package/dist/index.cjs +1152 -0
  3. package/dist/index.d.cts +544 -0
  4. package/dist/index.d.mts +544 -0
  5. package/dist/index.mjs +1104 -0
  6. package/package.json +4 -1
  7. package/.oxlintrc.json +0 -7
  8. package/.release-it.json +0 -13
  9. package/CHANGELOG.md +0 -16
  10. package/src/Flare.ts +0 -543
  11. package/src/Scope.ts +0 -96
  12. package/src/api/Api.ts +0 -35
  13. package/src/api/index.ts +0 -1
  14. package/src/env/index.ts +0 -14
  15. package/src/index.ts +0 -41
  16. package/src/stacktrace/NullFileReader.ts +0 -28
  17. package/src/stacktrace/createStackTrace.ts +0 -74
  18. package/src/stacktrace/fileReader.ts +0 -96
  19. package/src/stacktrace/index.ts +0 -4
  20. package/src/types.ts +0 -81
  21. package/src/util/assert.ts +0 -9
  22. package/src/util/assertKey.ts +0 -11
  23. package/src/util/convertToError.ts +0 -22
  24. package/src/util/extractCode.ts +0 -11
  25. package/src/util/flatJsonStringify.ts +0 -45
  26. package/src/util/glowsToEvents.ts +0 -16
  27. package/src/util/index.ts +0 -8
  28. package/src/util/now.ts +0 -3
  29. package/src/util/redactUrl.ts +0 -83
  30. package/tests/api.test.ts +0 -95
  31. package/tests/configure.test.ts +0 -16
  32. package/tests/contextCollector.test.ts +0 -37
  33. package/tests/convertToError.test.ts +0 -95
  34. package/tests/createStackTrace.test.ts +0 -54
  35. package/tests/extractCode.test.ts +0 -30
  36. package/tests/fileReader.test.ts +0 -51
  37. package/tests/flatJsonStringify.test.ts +0 -31
  38. package/tests/flush.test.ts +0 -47
  39. package/tests/glows.test.ts +0 -47
  40. package/tests/glowsToEvents.test.ts +0 -41
  41. package/tests/helpers/FakeApi.ts +0 -20
  42. package/tests/helpers/index.ts +0 -1
  43. package/tests/hooks.test.ts +0 -123
  44. package/tests/light.test.ts +0 -25
  45. package/tests/nullFileReader.test.ts +0 -11
  46. package/tests/publicExports.test.ts +0 -17
  47. package/tests/redactUrl.test.ts +0 -151
  48. package/tests/report.test.ts +0 -146
  49. package/tests/sampleRate.test.ts +0 -88
  50. package/tests/scope.test.ts +0 -64
  51. package/tests/setEntryPoint.test.ts +0 -79
  52. package/tests/setFramework.test.ts +0 -48
  53. package/tests/setSdkInfo.test.ts +0 -62
  54. package/tsconfig.json +0 -4
  55. package/vitest.config.ts +0 -17
package/tests/api.test.ts DELETED
@@ -1,95 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
2
-
3
- import { Api } from '../src/api';
4
-
5
- const minimalReport = {
6
- exceptionClass: 'Error',
7
- message: 'test',
8
- seenAtUnixNano: 0,
9
- stacktrace: [],
10
- events: [],
11
- attributes: {},
12
- };
13
-
14
- describe('Api.report', () => {
15
- const api = new Api();
16
-
17
- beforeEach(() => {
18
- vi.spyOn(globalThis, 'fetch');
19
- });
20
-
21
- afterEach(() => {
22
- vi.restoreAllMocks();
23
- });
24
-
25
- test('sends POST with correct headers', async () => {
26
- vi.mocked(fetch).mockResolvedValue(new Response('', { status: 201 }));
27
-
28
- await api.report(minimalReport, 'https://example.com/ingest', 'test-key', false);
29
-
30
- expect(fetch).toHaveBeenCalledWith(
31
- 'https://example.com/ingest',
32
- expect.objectContaining({
33
- method: 'POST',
34
- headers: expect.objectContaining({
35
- 'X-Api-Token': 'test-key',
36
- 'Content-Type': 'application/json',
37
- 'X-Flare-Client-Version': '2',
38
- }),
39
- }),
40
- );
41
- });
42
-
43
- test('does not log on 201 response', async () => {
44
- vi.mocked(fetch).mockResolvedValue(new Response('', { status: 201 }));
45
- const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
46
-
47
- await api.report(minimalReport, 'https://example.com/ingest', 'test-key', false, true);
48
-
49
- expect(errorSpy).not.toHaveBeenCalled();
50
- });
51
-
52
- test('logs on non-201 response only when debug is true', async () => {
53
- vi.mocked(fetch).mockResolvedValue(new Response('', { status: 429 }));
54
- const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
55
-
56
- await api.report(minimalReport, 'https://example.com/ingest', 'test-key', false, false);
57
- expect(errorSpy).not.toHaveBeenCalled();
58
-
59
- await api.report(minimalReport, 'https://example.com/ingest', 'test-key', false, true);
60
- expect(errorSpy).toHaveBeenCalledOnce();
61
- });
62
-
63
- test('swallows fetch rejection and logs when debug is true', async () => {
64
- vi.mocked(fetch).mockRejectedValue(new TypeError('network error'));
65
- const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
66
-
67
- await api.report(minimalReport, 'https://example.com/ingest', 'test-key', false, true);
68
-
69
- expect(errorSpy).toHaveBeenCalledOnce();
70
- });
71
-
72
- test('swallows fetch rejection silently when debug is false', async () => {
73
- vi.mocked(fetch).mockRejectedValue(new TypeError('network error'));
74
- const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
75
-
76
- await api.report(minimalReport, 'https://example.com/ingest', 'test-key', false, false);
77
-
78
- expect(errorSpy).not.toHaveBeenCalled();
79
- });
80
-
81
- test('uses empty string for null key', async () => {
82
- vi.mocked(fetch).mockResolvedValue(new Response('', { status: 201 }));
83
-
84
- await api.report(minimalReport, 'https://example.com/ingest', null, false);
85
-
86
- expect(fetch).toHaveBeenCalledWith(
87
- expect.any(String),
88
- expect.objectContaining({
89
- headers: expect.objectContaining({
90
- 'X-Api-Token': '',
91
- }),
92
- }),
93
- );
94
- });
95
- });
@@ -1,16 +0,0 @@
1
- import { expect, test } from 'vitest';
2
-
3
- import { Flare } from '../src';
4
-
5
- test('configure merges over defaults', () => {
6
- const client = new Flare();
7
- client.configure({ key: 'key', ingestUrl: 'https://example.test/v1/errors' });
8
-
9
- expect(client.config.key).toBe('key');
10
- expect(client.config.ingestUrl).toBe('https://example.test/v1/errors');
11
- });
12
-
13
- test('default ingestUrl points to production', () => {
14
- const client = new Flare();
15
- expect(client.config.ingestUrl).toBe('https://ingress.flareapp.io/v1/errors');
16
- });
@@ -1,37 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
-
3
- import { Api } from '../src/api';
4
- import { Flare } from '../src/Flare';
5
- import type { Config } from '../src/types';
6
-
7
- describe('Flare ContextCollector', () => {
8
- it('merges collector output into report attributes', async () => {
9
- const collector = vi.fn((_config: Config) => ({ 'custom.key': 'custom-value' }));
10
- const api = new Api();
11
- const sent: any[] = [];
12
- api.report = (report: any) => {
13
- sent.push(report);
14
- return Promise.resolve();
15
- };
16
-
17
- const flare = new Flare(api, collector);
18
- flare.light('test-key');
19
- await flare.report(new Error('boom'));
20
-
21
- expect(collector).toHaveBeenCalledTimes(1);
22
- expect(sent[0].attributes['custom.key']).toBe('custom-value');
23
- });
24
-
25
- it('defaults to a no-op collector', async () => {
26
- const api = new Api();
27
- const sent: any[] = [];
28
- api.report = (report: any) => {
29
- sent.push(report);
30
- return Promise.resolve();
31
- };
32
- const flare = new Flare(api);
33
- flare.light('test-key');
34
- await flare.report(new Error('boom'));
35
- expect(sent.length).toBe(1);
36
- });
37
- });
@@ -1,95 +0,0 @@
1
- import { describe, expect, test } from 'vitest';
2
-
3
- import { convertToError } from '../src';
4
-
5
- describe('convertToError', () => {
6
- test('returns the same Error instance if given an Error', () => {
7
- const error = new Error('original');
8
-
9
- const result = convertToError(error);
10
-
11
- expect(result).toBe(error);
12
- expect(result.message).toBe('original');
13
- });
14
-
15
- test('returns Error subclass instances as-is', () => {
16
- const error = new TypeError('type error');
17
-
18
- const result = convertToError(error);
19
-
20
- expect(result).toBe(error);
21
- expect(result).toBeInstanceOf(TypeError);
22
- });
23
-
24
- test('wraps a string in an Error', () => {
25
- const result = convertToError('something went wrong');
26
-
27
- expect(result).toBeInstanceOf(Error);
28
- expect(result.message).toBe('something went wrong');
29
- });
30
-
31
- test('wraps an empty string in an Error', () => {
32
- const result = convertToError('');
33
-
34
- expect(result).toBeInstanceOf(Error);
35
- expect(result.message).toBe('');
36
- });
37
-
38
- test('wraps a number in an Error via String()', () => {
39
- const result = convertToError(42);
40
-
41
- expect(result).toBeInstanceOf(Error);
42
- expect(result.message).toBe('42');
43
- });
44
-
45
- test('wraps null in an Error', () => {
46
- const result = convertToError(null);
47
-
48
- expect(result).toBeInstanceOf(Error);
49
- expect(result.message).toBe('null');
50
- });
51
-
52
- test('wraps undefined in an Error', () => {
53
- const result = convertToError(undefined);
54
-
55
- expect(result).toBeInstanceOf(Error);
56
- expect(result.message).toBe('undefined');
57
- });
58
-
59
- test('extracts message from objects with a message property', () => {
60
- const result = convertToError({ message: 'Internal Error', status: 500 });
61
-
62
- expect(result).toBeInstanceOf(Error);
63
- expect(result.message).toBe('Internal Error');
64
- });
65
-
66
- test('preserves stack from objects with a stack property', () => {
67
- const stack = 'Error: test\n at load (+page.server.ts:10:15)';
68
- const result = convertToError({ message: 'test', stack });
69
-
70
- expect(result).toBeInstanceOf(Error);
71
- expect(result.message).toBe('test');
72
- expect(result.stack).toBe(stack);
73
- });
74
-
75
- test('preserves name from objects with a name property', () => {
76
- const result = convertToError({ message: 'test', name: 'TypeError' });
77
-
78
- expect(result).toBeInstanceOf(Error);
79
- expect(result.name).toBe('TypeError');
80
- });
81
-
82
- test('wraps an object without message in an Error', () => {
83
- const result = convertToError({ key: 'value' });
84
-
85
- expect(result).toBeInstanceOf(Error);
86
- expect(result.message).toBe('[object Object]');
87
- });
88
-
89
- test('wraps a boolean in an Error', () => {
90
- const result = convertToError(false);
91
-
92
- expect(result).toBeInstanceOf(Error);
93
- expect(result.message).toBe('false');
94
- });
95
- });
@@ -1,54 +0,0 @@
1
- // @vitest-environment jsdom
2
- import ErrorStackParser from 'error-stack-parser';
3
- import { expect, test, vi } from 'vitest';
4
-
5
- import { createStackTrace } from '../src/stacktrace/createStackTrace';
6
- import { NullFileReader } from '../src/stacktrace/NullFileReader';
7
-
8
- vi.mock('error-stack-parser', () => ({
9
- default: {
10
- parse: vi.fn(() => {
11
- throw new Error('parser broke on malformed stack');
12
- }),
13
- },
14
- }));
15
-
16
- const nullReader = new NullFileReader();
17
-
18
- test('resolves (does not reject) when ErrorStackParser throws', async () => {
19
- const error = new Error('boom');
20
- // ensure hasStack() returns true so we reach the parse call
21
- error.stack = 'Error: boom\n at fn (file.js:1:1)\n at fn2 (file.js:2:2)';
22
-
23
- await expect(createStackTrace(error, false, nullReader)).resolves.toBeDefined();
24
- });
25
-
26
- test('returns a fallback frame array when ErrorStackParser throws', async () => {
27
- const error = new Error('boom');
28
- error.stack = 'Error: boom\n at fn (file.js:1:1)';
29
-
30
- const frames = await createStackTrace(error, false, nullReader);
31
-
32
- expect(Array.isArray(frames)).toBe(true);
33
- expect(frames.length).toBeGreaterThan(0);
34
- });
35
-
36
- test('marks node_modules frames as non-application', async () => {
37
- (ErrorStackParser.parse as unknown as ReturnType<typeof vi.fn>).mockImplementationOnce(() => [
38
- {
39
- fileName: 'https://cdn.test/app/node_modules/lodash/index.js',
40
- lineNumber: 1,
41
- columnNumber: 1,
42
- functionName: 'lodash',
43
- },
44
- { fileName: 'https://cdn.test/app/src/main.ts', lineNumber: 2, columnNumber: 2, functionName: 'main' },
45
- ]);
46
-
47
- const error = new Error('boom');
48
- error.stack = 'Error: boom\n at fn (file.js:1:1)';
49
-
50
- const frames = await createStackTrace(error, false, nullReader);
51
-
52
- expect(frames[0].isApplicationFrame).toBe(false);
53
- expect(frames[1].isApplicationFrame).toBe(true);
54
- });
@@ -1,30 +0,0 @@
1
- import { describe, expect, test } from 'vitest';
2
-
3
- import { extractCode } from '../src/util';
4
-
5
- describe('extractCode', () => {
6
- test('returns string code from error', () => {
7
- const err = Object.assign(new Error('boom'), { code: 'ENOTFOUND' });
8
- expect(extractCode(err)).toBe('ENOTFOUND');
9
- });
10
-
11
- test('returns undefined when no code', () => {
12
- expect(extractCode(new Error('boom'))).toBeUndefined();
13
- });
14
-
15
- test('returns undefined for non-string code (e.g. legacy Node errno number)', () => {
16
- const err = Object.assign(new Error('boom'), { code: 42 });
17
- expect(extractCode(err)).toBeUndefined();
18
- });
19
-
20
- test('returns undefined for empty string code', () => {
21
- const err = Object.assign(new Error('boom'), { code: '' });
22
- expect(extractCode(err)).toBeUndefined();
23
- });
24
-
25
- test('truncates code longer than 64 chars', () => {
26
- const long = 'A'.repeat(100);
27
- const err = Object.assign(new Error('boom'), { code: long });
28
- expect(extractCode(err)).toBe('A'.repeat(64));
29
- });
30
- });
@@ -1,51 +0,0 @@
1
- import { beforeEach, describe, expect, it } from 'vitest';
2
-
3
- import { __clearFileReaderCacheForTests, getCodeSnippet, readLinesFromFile } from '../src/stacktrace/fileReader';
4
- import type { FileReader } from '../src/stacktrace/fileReader';
5
-
6
- class StubReader implements FileReader {
7
- constructor(private map: Record<string, string | null>) {}
8
- read(url: string): Promise<string | null> {
9
- return Promise.resolve(this.map[url] ?? null);
10
- }
11
- }
12
-
13
- describe('getCodeSnippet', () => {
14
- beforeEach(() => __clearFileReaderCacheForTests());
15
-
16
- it('returns an error message when url is missing', async () => {
17
- const reader = new StubReader({});
18
- const res = await getCodeSnippet(reader, undefined, 1, 1);
19
- expect(res.codeSnippet[0]).toContain('missing file URL');
20
- });
21
-
22
- it('returns the line and surrounding context', async () => {
23
- const reader = new StubReader({ 'a.js': 'a\nb\nc\nd\ne' });
24
- const res = await getCodeSnippet(reader, 'a.js', 3, 1);
25
- expect(res.codeSnippet[3]).toBe('c');
26
- expect(res.codeSnippet[2]).toBe('b');
27
- expect(res.codeSnippet[4]).toBe('d');
28
- });
29
-
30
- it('caches reads', async () => {
31
- let calls = 0;
32
- const reader: FileReader = {
33
- read(_url: string) {
34
- calls += 1;
35
- return Promise.resolve('x\ny\nz');
36
- },
37
- };
38
- await getCodeSnippet(reader, 'a.js', 2, 1);
39
- await getCodeSnippet(reader, 'a.js', 2, 1);
40
- expect(calls).toBe(1);
41
- });
42
- });
43
-
44
- describe('readLinesFromFile', () => {
45
- it('truncates very long lines from the start', () => {
46
- const long = 'x'.repeat(2000);
47
- const res = readLinesFromFile(long, 1);
48
- expect(res.codeSnippet[1].length).toBeLessThanOrEqual(1001);
49
- expect(res.codeSnippet[1].endsWith('…')).toBe(true);
50
- });
51
- });
@@ -1,31 +0,0 @@
1
- import { describe, expect, test } from 'vitest';
2
-
3
- import { flatJsonStringify } from '../src/util';
4
-
5
- describe('flatJsonStringify', () => {
6
- test('preserves shared (non-cyclic) sub-objects on both branches', () => {
7
- const inner = { x: 1 };
8
- const obj = { a: inner, b: inner };
9
-
10
- const parsed = JSON.parse(flatJsonStringify(obj));
11
-
12
- expect(parsed).toEqual({ a: { x: 1 }, b: { x: 1 } });
13
- });
14
-
15
- test('replaces real cycles with a sentinel rather than silently dropping the field', () => {
16
- const obj: { a: number; self?: unknown } = { a: 1 };
17
- obj.self = obj;
18
-
19
- const parsed = JSON.parse(flatJsonStringify(obj));
20
-
21
- expect(parsed.a).toBe(1);
22
- expect(parsed.self).toBe('[Circular]');
23
- });
24
-
25
- test('does not throw on cyclic structures', () => {
26
- const obj: { self?: unknown } = {};
27
- obj.self = obj;
28
-
29
- expect(() => flatJsonStringify(obj)).not.toThrow();
30
- });
31
- });
@@ -1,47 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
- import { Api } from '../src/api';
4
- import { Flare } from '../src/Flare';
5
-
6
- describe('Flare.flush', () => {
7
- it('awaits in-flight reports', async () => {
8
- let resolveApi: () => void = () => {};
9
- const apiPromise = new Promise<void>((res) => {
10
- resolveApi = res;
11
- });
12
- const api = new Api();
13
- api.report = () => apiPromise;
14
- const flare = new Flare(api);
15
- flare.light('k');
16
-
17
- let flushDone = false;
18
- const reportPromise = flare.report(new Error('x'));
19
- const flushPromise = flare.flush(1000).then(() => {
20
- flushDone = true;
21
- });
22
-
23
- // flush should not resolve while the report is in flight
24
- await new Promise((r) => setTimeout(r, 10));
25
- expect(flushDone).toBe(false);
26
-
27
- resolveApi();
28
- await reportPromise;
29
- await flushPromise;
30
- expect(flushDone).toBe(true);
31
- });
32
-
33
- it('returns after timeout even if a report is stuck', async () => {
34
- const api = new Api();
35
- api.report = () => new Promise(() => {}); // never resolves
36
- const flare = new Flare(api);
37
- flare.light('k');
38
- const reportPromise = flare.report(new Error('x'));
39
- const start = Date.now();
40
- await flare.flush(50);
41
- const elapsed = Date.now() - start;
42
- expect(elapsed).toBeGreaterThanOrEqual(45);
43
- expect(elapsed).toBeLessThan(500);
44
- // swallow the never-resolving promise
45
- void reportPromise;
46
- });
47
- });
@@ -1,47 +0,0 @@
1
- // @vitest-environment jsdom
2
- import { beforeEach, expect, test } 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('glows are serialized into php_glow span events on report', async () => {
16
- client.glow('rendering checkout', 'info', { cartId: 7 });
17
-
18
- await client.reportMessage('hello');
19
-
20
- const event = fakeApi.lastReport!.events[0];
21
- expect(event.type).toBe('php_glow');
22
- expect(event.endTimeUnixNano).toBeNull();
23
- expect(typeof event.startTimeUnixNano).toBe('number');
24
- expect(event.attributes['glow.name']).toBe('rendering checkout');
25
- expect(event.attributes['glow.level']).toBe('info');
26
- expect(event.attributes['glow.context']).toEqual({ cartId: 7 });
27
- });
28
-
29
- test('glow buffer respects maxGlowsPerReport', () => {
30
- client.configure({ maxGlowsPerReport: 2 });
31
- client.glow('a').glow('b').glow('c');
32
-
33
- expect(client.glows).toHaveLength(2);
34
- expect(client.glows.map((g) => g.name)).toEqual(['b', 'c']);
35
- });
36
-
37
- test('clearGlows empties the buffer', () => {
38
- client.glow('a').glow('b');
39
- client.clearGlows();
40
-
41
- expect(client.glows).toHaveLength(0);
42
- });
43
-
44
- test('events array is empty when no glows recorded', async () => {
45
- await client.report(new Error('x'));
46
- expect(fakeApi.lastReport!.events).toEqual([]);
47
- });
@@ -1,41 +0,0 @@
1
- import { describe, expect, test } from 'vitest';
2
-
3
- import { glowsToEvents } from '../src/util';
4
-
5
- describe('glowsToEvents', () => {
6
- test('produces a php_glow span event per glow', () => {
7
- const events = glowsToEvents([
8
- { name: 'rendering checkout', messageLevel: 'info', metaData: { cartId: 7 }, time: 1, microtime: 1 },
9
- ]);
10
-
11
- expect(events).toHaveLength(1);
12
- expect(events[0]).toEqual({
13
- type: 'php_glow',
14
- startTimeUnixNano: 1_000_000_000,
15
- endTimeUnixNano: null,
16
- attributes: {
17
- 'glow.name': 'rendering checkout',
18
- 'glow.level': 'info',
19
- 'glow.context': { cartId: 7 },
20
- },
21
- });
22
- });
23
-
24
- test('defaults missing metaData to empty object', () => {
25
- const events = glowsToEvents([
26
- {
27
- name: 'x',
28
- messageLevel: 'warning',
29
- metaData: undefined as unknown as Record<string, unknown>,
30
- time: 2,
31
- microtime: 2,
32
- },
33
- ]);
34
-
35
- expect(events[0].attributes['glow.context']).toEqual({});
36
- });
37
-
38
- test('returns empty array for empty input', () => {
39
- expect(glowsToEvents([])).toEqual([]);
40
- });
41
- });
@@ -1,20 +0,0 @@
1
- import { Api } from '../../src/api';
2
- import { Report } from '../../src/types';
3
-
4
- export class FakeApi extends Api {
5
- reports: Report[] = [];
6
-
7
- lastReport?: Report;
8
- lastUrl?: string;
9
- lastKey?: string | null;
10
- lastReportBrowserExtensionErrors?: boolean;
11
-
12
- report(report: Report, url: string, key: string | null, reportBrowserExtensionErrors: boolean): Promise<void> {
13
- this.reports.push(report);
14
- this.lastUrl = url;
15
- this.lastKey = key;
16
- this.lastReportBrowserExtensionErrors = reportBrowserExtensionErrors;
17
- this.lastReport = report;
18
- return Promise.resolve();
19
- }
20
- }
@@ -1 +0,0 @@
1
- export * from './FakeApi';