@flareapp/js 1.0.0-beta.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) Spatie bvba <info@spatie.be>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # The JavaScript client for Flare to catch frontend errors
2
+
3
+ Read the JavaScript error tracking section in [the Flare documentation](https://flareapp.io/docs/javascript-error-tracking/installation) for more information.
4
+
5
+ React plugin: https://www.npmjs.com/package/@flareapp/react
6
+
7
+ Vue plugin: https://www.npmjs.com/package/@flareapp/vue
8
+
9
+ Webpack plugin: https://www.npmjs.com/package/@flareapp/flare-webpack-plugin-sourcemap
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@flareapp/js",
3
+ "version": "1.0.0-beta.0",
4
+ "description": "JavaScript client for flareapp.io",
5
+ "homepage": "https://flareapp.io",
6
+ "bugs": {
7
+ "url": "https://github.com/facade/flare-client-js/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/facade/flare-client-js.git"
12
+ },
13
+ "license": "MIT",
14
+ "author": "adriaan@spatie.be",
15
+ "main": "./dist/index.js",
16
+ "module": "./dist/index.mjs",
17
+ "types": "./dist/index.d.ts",
18
+ "scripts": {
19
+ "build": "tsup src/index.ts --format cjs,esm --dts --env.FLARE_JS_CLIENT_VERSION=\\\"$(node -p \"require('./package.json').version\")\\\" --clean",
20
+ "dev": "npm run build",
21
+ "test": "vitest run",
22
+ "typescript": "tsc"
23
+ },
24
+ "dependencies": {
25
+ "error-stack-parser": "^2.0.2"
26
+ },
27
+ "devDependencies": {
28
+ "tsup": "^8.0.1",
29
+ "typescript": "^5.3.3",
30
+ "vitest": "^1.0.4"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ }
35
+ }
@@ -0,0 +1,285 @@
1
+ import { assert, now, flatJsonStringify } from './util';
2
+ import { collectContext } from './context';
3
+ import { createStackTrace } from './stacktrace';
4
+ import build from './build';
5
+ import getSolutions from './solutions';
6
+ import { Flare } from './types';
7
+
8
+ export default class FlareClient {
9
+ public version: string = build.clientVersion;
10
+
11
+ public config: Flare.Config = {
12
+ key: '',
13
+ reportingUrl: 'https://flareapp.io/api/reports',
14
+ maxGlowsPerReport: 30,
15
+ maxReportsPerMinute: 500,
16
+ };
17
+
18
+ private glows: Array<Flare.Glow> = [];
19
+
20
+ private context: Flare.Context = { context: {} };
21
+
22
+ public beforeEvaluate: Flare.BeforeEvaluate = (error) => error;
23
+
24
+ public beforeSubmit: Flare.BeforeSubmit = (report) => report;
25
+
26
+ private reportedErrorsTimestamps: Array<number> = [];
27
+
28
+ private solutionProviders: Array<Flare.SolutionProvider> = [];
29
+
30
+ private sourcemapVersion: string = build.sourcemapVersion;
31
+
32
+ public debug: boolean = false;
33
+
34
+ public stage: string | undefined = undefined;
35
+
36
+ public light(key: string = build.flareJsKey, debug = false): FlareClient {
37
+ this.debug = debug;
38
+
39
+ if (
40
+ !assert(
41
+ key && typeof key === 'string',
42
+ 'An empty or incorrect Flare key was passed, errors will not be reported.',
43
+ this.debug,
44
+ ) ||
45
+ !assert(
46
+ Promise,
47
+ 'ES6 promises are not supported in this environment, errors will not be reported.',
48
+ this.debug,
49
+ )
50
+ ) {
51
+ return this;
52
+ }
53
+
54
+ this.config.key = key;
55
+
56
+ return this;
57
+ }
58
+
59
+ public glow(
60
+ name: string,
61
+ level: Flare.MessageLevel = 'info',
62
+ metaData: Array<object> = [],
63
+ ): FlareClient {
64
+ const time = now();
65
+
66
+ this.glows.push({
67
+ name,
68
+ message_level: level,
69
+ meta_data: metaData,
70
+ time,
71
+ microtime: time,
72
+ });
73
+
74
+ if (this.glows.length > this.config.maxGlowsPerReport) {
75
+ this.glows = this.glows.slice(
76
+ this.glows.length - this.config.maxGlowsPerReport,
77
+ );
78
+ }
79
+
80
+ return this;
81
+ }
82
+
83
+ public addContext(name: string, value: any): FlareClient {
84
+ this.context.context[name] = value;
85
+
86
+ return this;
87
+ }
88
+
89
+ public addContextGroup(groupName: string, value: object): FlareClient {
90
+ this.context[groupName] = value;
91
+
92
+ return this;
93
+ }
94
+
95
+ public registerSolutionProvider(
96
+ provider: Flare.SolutionProvider,
97
+ ): FlareClient {
98
+ if (
99
+ !assert(
100
+ 'canSolve' in provider,
101
+ 'A solution provider without a [canSolve] property was added.',
102
+ this.debug,
103
+ ) ||
104
+ !assert(
105
+ 'getSolutions' in provider,
106
+ 'A solution provider without a [getSolutions] property was added.',
107
+ this.debug,
108
+ )
109
+ ) {
110
+ return this;
111
+ }
112
+
113
+ this.solutionProviders.push(provider);
114
+
115
+ return this;
116
+ }
117
+
118
+ public reportMessage(
119
+ message: string,
120
+ context: Flare.Context = {},
121
+ exceptionClass: string = 'Log',
122
+ ): void {
123
+ const seenAt = now();
124
+
125
+ createStackTrace(Error()).then((stacktrace) => {
126
+ // The first item in the stacktrace is from this file, and irrelevant
127
+ stacktrace.shift();
128
+
129
+ const report: Flare.ErrorReport = {
130
+ notifier: `Flare JavaScript client v${build.clientVersion}`,
131
+ exception_class: exceptionClass,
132
+ seen_at: seenAt,
133
+ message: message,
134
+ language: 'javascript',
135
+ glows: this.glows,
136
+ context: collectContext({ ...context, ...this.context }),
137
+ stacktrace,
138
+ sourcemap_version_id: this.sourcemapVersion,
139
+ solutions: [],
140
+ stage: this.stage,
141
+ };
142
+
143
+ this.sendReport(report);
144
+ });
145
+ }
146
+
147
+ public report(
148
+ error: Error,
149
+ context: Flare.Context = {},
150
+ extraSolutionParameters: Flare.SolutionProviderExtraParameters = {},
151
+ ): void {
152
+ Promise.resolve(this.beforeEvaluate(error)).then(
153
+ (reportReadyForEvaluation) => {
154
+ if (!reportReadyForEvaluation) {
155
+ return;
156
+ }
157
+
158
+ this.createReport(error, context, extraSolutionParameters).then(
159
+ (report) => (report ? this.sendReport(report) : {}),
160
+ );
161
+ },
162
+ );
163
+ }
164
+
165
+ public createReport(
166
+ error: Error,
167
+ context: Flare.Context = {},
168
+ extraSolutionParameters: Flare.SolutionProviderExtraParameters = {},
169
+ ): Promise<Flare.ErrorReport | false> {
170
+ if (!assert(error, 'No error provided.', this.debug)) {
171
+ return Promise.resolve(false);
172
+ }
173
+
174
+ const seenAt = now();
175
+
176
+ return Promise.all([
177
+ getSolutions(
178
+ this.solutionProviders,
179
+ error,
180
+ extraSolutionParameters,
181
+ ),
182
+ createStackTrace(error),
183
+ ]).then((result) => {
184
+ const [solutions, stacktrace] = result;
185
+
186
+ assert(
187
+ stacktrace.length,
188
+ "Couldn't generate stacktrace of this error: " + error,
189
+ this.debug,
190
+ );
191
+
192
+ return {
193
+ notifier: `Flare JavaScript client v${build.clientVersion}`,
194
+ exception_class:
195
+ error.constructor && error.constructor.name
196
+ ? error.constructor.name
197
+ : 'undefined',
198
+ seen_at: seenAt,
199
+ message: error.message,
200
+ language: 'javascript',
201
+ glows: this.glows,
202
+ context: collectContext({ ...context, ...this.context }),
203
+ stacktrace,
204
+ sourcemap_version_id: this.sourcemapVersion,
205
+ solutions,
206
+ stage: this.stage,
207
+ };
208
+ });
209
+ }
210
+
211
+ private sendReport(report: Flare.ErrorReport): void {
212
+ if (
213
+ !assert(
214
+ this.config.key,
215
+ 'The client was not yet initialised with an API key. ' +
216
+ "Run client.light('<flare-project-key>') when you initialise your app. " +
217
+ "If you are running in dev mode and didn't run the light command on purpose, you can ignore this error.",
218
+ this.debug,
219
+ )
220
+ ) {
221
+ return;
222
+ }
223
+
224
+ if (this.maxReportsPerMinuteReached()) {
225
+ return;
226
+ }
227
+
228
+ Promise.resolve(this.beforeSubmit(report)).then(
229
+ (reportReadyForSubmit) => {
230
+ if (!reportReadyForSubmit) {
231
+ return;
232
+ }
233
+
234
+ fetch(this.config.reportingUrl, {
235
+ method: 'POST',
236
+ headers: {
237
+ 'Content-Type': 'application/json',
238
+ 'X-Requested-With': 'XMLHttpRequest',
239
+ 'x-api-token': this.config.key,
240
+ },
241
+ body: flatJsonStringify({
242
+ ...reportReadyForSubmit,
243
+ key: this.config.key,
244
+ }),
245
+ }).then(
246
+ (response) => {
247
+ if (response.status !== 204) {
248
+ console.error(
249
+ `Received response with status ${response.status} from Flare`,
250
+ );
251
+ }
252
+ },
253
+ (error) => console.error(error),
254
+ );
255
+
256
+ this.reportedErrorsTimestamps.push(Date.now());
257
+ },
258
+ );
259
+ }
260
+
261
+ private maxReportsPerMinuteReached(): boolean {
262
+ if (
263
+ this.reportedErrorsTimestamps.length >=
264
+ this.config.maxReportsPerMinute
265
+ ) {
266
+ const nErrorsBack =
267
+ this.reportedErrorsTimestamps[
268
+ this.reportedErrorsTimestamps.length -
269
+ this.config.maxReportsPerMinute
270
+ ];
271
+
272
+ if (nErrorsBack > Date.now() - 60 * 1000) {
273
+ return true;
274
+ }
275
+ }
276
+
277
+ return false;
278
+ }
279
+
280
+ public test(): FlareClient {
281
+ this.report(new Error('The Flare client is set up correctly!'));
282
+
283
+ return this;
284
+ }
285
+ }
@@ -0,0 +1,37 @@
1
+ export default function catchWindowErrors() {
2
+ if (typeof window === 'undefined') {
3
+ return;
4
+ }
5
+
6
+ const flare = window.flare;
7
+
8
+ if (!window || !flare) {
9
+ return;
10
+ }
11
+
12
+ const originalOnerrorHandler = window.onerror;
13
+ const originalOnunhandledrejectionHandler = window.onunhandledrejection;
14
+
15
+ window.onerror = (_1, _2, _3, _4, error) => {
16
+ if (error) {
17
+ flare.report(error);
18
+ }
19
+
20
+ if (typeof originalOnerrorHandler === 'function') {
21
+ originalOnerrorHandler(_1, _2, _3, _4, error);
22
+ }
23
+ };
24
+
25
+ window.onunhandledrejection = (error: PromiseRejectionEvent) => {
26
+ if (error.reason instanceof Error) {
27
+ flare.report(error.reason);
28
+ }
29
+
30
+ // TODO: Maybe also send errors without stacktrace for unhandled rejections without an Error as reason? (could be a string, …)
31
+
32
+ if (typeof originalOnunhandledrejectionHandler === 'function') {
33
+ // @ts-ignore
34
+ originalOnunhandledrejectionHandler(error);
35
+ }
36
+ };
37
+ }
package/src/build.ts ADDED
@@ -0,0 +1,16 @@
1
+ declare const FLARE_JS_KEY: string | undefined;
2
+ declare const FLARE_SOURCEMAP_VERSION: string | undefined;
3
+
4
+ export default {
5
+ // Injected during build
6
+ clientVersion:
7
+ typeof process.env.FLARE_JS_CLIENT_VERSION === 'undefined'
8
+ ? '?'
9
+ : process.env.FLARE_JS_CLIENT_VERSION,
10
+ // Optionally injected by flare-vite-plugin-sourcemap-uploader
11
+ flareJsKey: typeof FLARE_JS_KEY === 'undefined' ? '' : FLARE_JS_KEY,
12
+ sourcemapVersion:
13
+ typeof FLARE_SOURCEMAP_VERSION === 'undefined'
14
+ ? ''
15
+ : FLARE_SOURCEMAP_VERSION,
16
+ };
@@ -0,0 +1,17 @@
1
+ export default function cookie() {
2
+ if (!window.document.cookie) {
3
+ return {};
4
+ }
5
+
6
+ return {
7
+ cookies: window.document.cookie.split('; ').reduce(
8
+ (cookies, cookie) => {
9
+ const [cookieName, cookieValue] = cookie.split(/=/);
10
+ cookies[cookieName] = cookieValue;
11
+
12
+ return cookies;
13
+ },
14
+ {} as { [key: string]: string },
15
+ ),
16
+ };
17
+ }
@@ -0,0 +1,17 @@
1
+ import cookie from './cookie';
2
+ import request from './request';
3
+ import requestData from './requestData';
4
+ import { Flare } from '../types';
5
+
6
+ export function collectContext(additionalContext: object): Flare.Context {
7
+ if (typeof window === 'undefined') {
8
+ return additionalContext;
9
+ }
10
+
11
+ return {
12
+ ...cookie(),
13
+ ...request(),
14
+ ...requestData(),
15
+ ...additionalContext,
16
+ };
17
+ }
@@ -0,0 +1,10 @@
1
+ export default function request() {
2
+ return {
3
+ request: {
4
+ url: window.document.location.href,
5
+ useragent: window.navigator.userAgent,
6
+ referrer: window.document.referrer,
7
+ readyState: window.document.readyState,
8
+ },
9
+ };
10
+ }
@@ -0,0 +1,13 @@
1
+ export default function requestData() {
2
+ if (!window.location.search) {
3
+ return {};
4
+ }
5
+
6
+ const queryString: { [key: string]: string } = {};
7
+
8
+ new URLSearchParams(window.location.search).forEach((value, key) => {
9
+ queryString[key] = value;
10
+ });
11
+
12
+ return { request_data: { queryString } };
13
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ import FlareClient from './FlareClient';
2
+ import catchWindowErrors from './browserClient';
3
+ export { readLinesFromFile } from './stacktrace/fileReader';
4
+
5
+ export const flare = new FlareClient();
6
+
7
+ if (typeof window !== 'undefined' && window) {
8
+ window.flare = flare;
9
+ }
10
+
11
+ catchWindowErrors();
@@ -0,0 +1,3 @@
1
+ interface Window {
2
+ flare: import('./FlareClient').default;
3
+ }
@@ -0,0 +1,44 @@
1
+ import { flattenOnce } from '../util';
2
+ import { Flare } from '../types';
3
+
4
+ export default function getSolutions(
5
+ solutionProviders: Array<Flare.SolutionProvider>,
6
+ error: Error,
7
+ extraSolutionParameters: Flare.SolutionProviderExtraParameters = {},
8
+ ): Promise<Array<Flare.Solution>> {
9
+ return new Promise((resolve) => {
10
+ const canSolves = solutionProviders.reduce(
11
+ (canSolves, provider) => {
12
+ canSolves.push(
13
+ Promise.resolve(
14
+ provider.canSolve(error, extraSolutionParameters),
15
+ ),
16
+ );
17
+
18
+ return canSolves;
19
+ },
20
+ [] as Array<Promise<boolean>>,
21
+ );
22
+
23
+ Promise.all(canSolves).then((resolvedCanSolves) => {
24
+ const solutionPromises: Array<Promise<Array<Flare.Solution>>> = [];
25
+
26
+ resolvedCanSolves.forEach((canSolve, i) => {
27
+ if (canSolve) {
28
+ solutionPromises.push(
29
+ Promise.resolve(
30
+ solutionProviders[i].getSolutions(
31
+ error,
32
+ extraSolutionParameters,
33
+ ),
34
+ ),
35
+ );
36
+ }
37
+ });
38
+
39
+ Promise.all(solutionPromises).then((solutions) => {
40
+ resolve(flattenOnce(solutions));
41
+ });
42
+ });
43
+ });
44
+ }
@@ -0,0 +1,110 @@
1
+ const cachedFiles: { [key: string]: string } = {};
2
+
3
+ type CodeSnippet = {
4
+ [key: number]: string;
5
+ };
6
+
7
+ type ReaderResponse = {
8
+ codeSnippet: CodeSnippet;
9
+ trimmedColumnNumber: number | null;
10
+ };
11
+
12
+ export function getCodeSnippet(
13
+ url?: string,
14
+ lineNumber?: number,
15
+ columnNumber?: number,
16
+ ): Promise<ReaderResponse> {
17
+ return new Promise((resolve) => {
18
+ if (!url || !lineNumber) {
19
+ return resolve({
20
+ codeSnippet: {
21
+ 0: `Could not read from file: missing file URL or line number. URL: ${url} lineNumber: ${lineNumber}`,
22
+ },
23
+ trimmedColumnNumber: null,
24
+ });
25
+ }
26
+
27
+ readFile(url).then((fileText) => {
28
+ if (!fileText) {
29
+ return resolve({
30
+ codeSnippet: {
31
+ 0: `Could not read from file: Error while opening file at URL ${url}`,
32
+ },
33
+ trimmedColumnNumber: null,
34
+ });
35
+ }
36
+
37
+ return resolve(
38
+ readLinesFromFile(fileText, lineNumber, columnNumber),
39
+ );
40
+ });
41
+ });
42
+ }
43
+
44
+ function readFile(url: string): Promise<string | null> {
45
+ if (cachedFiles[url]) {
46
+ return Promise.resolve(cachedFiles[url]);
47
+ }
48
+
49
+ return fetch(url)
50
+ .then((response) => {
51
+ if (response.status !== 200) {
52
+ return null;
53
+ }
54
+
55
+ return response.text();
56
+ })
57
+ .catch(() => null);
58
+ }
59
+
60
+ export function readLinesFromFile(
61
+ fileText: string,
62
+ lineNumber: number,
63
+ columnNumber?: number,
64
+ maxSnippetLineLength = 1000,
65
+ maxSnippetLines = 40,
66
+ ): ReaderResponse {
67
+ const codeSnippet: CodeSnippet = {};
68
+ let trimmedColumnNumber = null;
69
+
70
+ const lines = fileText.split('\n');
71
+
72
+ for (let i = -maxSnippetLines / 2; i <= maxSnippetLines / 2; i++) {
73
+ const currentLineIndex = lineNumber + i;
74
+
75
+ if (currentLineIndex >= 0 && lines[currentLineIndex]) {
76
+ const displayLine = currentLineIndex + 1; // the linenumber in a stacktrace is not zero-based like an array
77
+
78
+ if (lines[currentLineIndex].length > maxSnippetLineLength) {
79
+ if (
80
+ columnNumber &&
81
+ columnNumber + maxSnippetLineLength / 2 >
82
+ maxSnippetLineLength
83
+ ) {
84
+ codeSnippet[displayLine] = lines[currentLineIndex].substr(
85
+ columnNumber - Math.round(maxSnippetLineLength / 2),
86
+ maxSnippetLineLength,
87
+ );
88
+
89
+ if (displayLine === lineNumber) {
90
+ trimmedColumnNumber = Math.round(
91
+ maxSnippetLineLength / 2,
92
+ );
93
+ }
94
+
95
+ continue;
96
+ }
97
+
98
+ codeSnippet[displayLine] =
99
+ lines[currentLineIndex].substr(0, maxSnippetLineLength) +
100
+ '…';
101
+
102
+ continue;
103
+ }
104
+
105
+ codeSnippet[displayLine] = lines[currentLineIndex];
106
+ }
107
+ }
108
+
109
+ return { codeSnippet, trimmedColumnNumber };
110
+ }
@@ -0,0 +1,71 @@
1
+ import ErrorStackParser from 'error-stack-parser';
2
+ import { getCodeSnippet } from './fileReader';
3
+ import { Flare } from '../types';
4
+ import { flare } from '..';
5
+ import { assert } from '../util';
6
+
7
+ export function createStackTrace(
8
+ error: Error,
9
+ ): Promise<Array<Flare.StackFrame>> {
10
+ return new Promise((resolve) => {
11
+ if (!hasStack(error)) {
12
+ assert(
13
+ false,
14
+ "Couldn't generate stacktrace of below error:",
15
+ flare.debug,
16
+ );
17
+
18
+ if (flare.debug) {
19
+ console.error(error);
20
+ }
21
+
22
+ return resolve([
23
+ {
24
+ line_number: 0,
25
+ column_number: 0,
26
+ method: 'unknown',
27
+ file: 'unknown',
28
+ code_snippet: {
29
+ 0: 'Could not read from file: stacktrace missing',
30
+ },
31
+ trimmed_column_number: null,
32
+ class: 'unknown',
33
+ },
34
+ ]);
35
+ }
36
+
37
+ Promise.all(
38
+ ErrorStackParser.parse(error).map((frame) => {
39
+ return new Promise<Flare.StackFrame>((resolve) => {
40
+ getCodeSnippet(
41
+ frame.fileName,
42
+ frame.lineNumber,
43
+ frame.columnNumber,
44
+ ).then((snippet) => {
45
+ resolve({
46
+ line_number: frame.lineNumber || 1,
47
+ column_number: frame.columnNumber || 1,
48
+ method:
49
+ frame.functionName ||
50
+ 'Anonymous or unknown function',
51
+ file: frame.fileName || 'Unknown file',
52
+ code_snippet: snippet.codeSnippet,
53
+ trimmed_column_number: snippet.trimmedColumnNumber,
54
+ class: '',
55
+ });
56
+ });
57
+ });
58
+ }),
59
+ ).then(resolve);
60
+ });
61
+ }
62
+
63
+ function hasStack(err: any): boolean {
64
+ return (
65
+ !!err &&
66
+ (!!err.stack || !!err.stacktrace || !!err['opera#sourceloc']) &&
67
+ typeof (err.stack || err.stacktrace || err['opera#sourceloc']) ===
68
+ 'string' &&
69
+ err.stack !== `${err.name}: ${err.message}`
70
+ );
71
+ }
package/src/types.ts ADDED
@@ -0,0 +1,92 @@
1
+ export namespace Flare {
2
+ export type BeforeEvaluate = (
3
+ error: Error,
4
+ ) => Error | false | Promise<Error | false>;
5
+
6
+ export type BeforeSubmit = (
7
+ report: ErrorReport,
8
+ ) => ErrorReport | false | Promise<ErrorReport | false>;
9
+
10
+ export type Config = {
11
+ key: string;
12
+ reportingUrl: string;
13
+ maxGlowsPerReport: number;
14
+ maxReportsPerMinute: number;
15
+ stage?: string;
16
+ };
17
+
18
+ export type ErrorReport = {
19
+ notifier: string;
20
+ exception_class: string;
21
+ seen_at: number;
22
+ message: string;
23
+ language: 'javascript';
24
+ glows: Array<Flare.Glow>;
25
+ context: Flare.Context;
26
+ stacktrace: Array<Flare.StackFrame>;
27
+ sourcemap_version_id: string;
28
+ solutions: Array<Flare.Solution>;
29
+ stage?: string;
30
+ };
31
+
32
+ export interface SolutionProviderExtraParameters {}
33
+
34
+ export type SolutionProvider = {
35
+ canSolve: (
36
+ error: Error,
37
+ extraParameters?: SolutionProviderExtraParameters,
38
+ ) => boolean | Promise<boolean>;
39
+ getSolutions: (
40
+ error: Error,
41
+ extraParameters?: SolutionProviderExtraParameters,
42
+ ) => Array<Flare.Solution> | Promise<Array<Flare.Solution>>;
43
+ };
44
+
45
+ export type Solution = {
46
+ class: string;
47
+ title: string;
48
+ description: string;
49
+ links: { [label: string]: string };
50
+ action_description?: string;
51
+ is_runnable?: boolean;
52
+ };
53
+
54
+ export type Context = {
55
+ request?: {
56
+ url?: String;
57
+ useragent?: String;
58
+ referrer?: String; // TODO: Flare doesn't catch this yet
59
+ readyState?: String; // TODO: Flare doesn't catch this yet
60
+ };
61
+ request_data?: {
62
+ queryString: { [key: string]: string };
63
+ };
64
+ cookies?: { [key: string]: string };
65
+ [key: string]: any;
66
+ };
67
+
68
+ export type StackFrame = {
69
+ line_number: number;
70
+ column_number: number; // TODO: Flare doesn't catch this yet
71
+ method: string;
72
+ file: string;
73
+ code_snippet: { [key: number]: string };
74
+ trimmed_column_number: number | null;
75
+ class: string;
76
+ };
77
+
78
+ export type Glow = {
79
+ time: number;
80
+ microtime: number;
81
+ name: String;
82
+ message_level: MessageLevel;
83
+ meta_data: Array<Object>;
84
+ };
85
+
86
+ export type MessageLevel =
87
+ | 'info'
88
+ | 'debug'
89
+ | 'warning'
90
+ | 'error'
91
+ | 'critical';
92
+ }
@@ -0,0 +1,44 @@
1
+ import build from '../build';
2
+
3
+ export function assert(value: any, message: string, debug: boolean) {
4
+ if (debug && !value) {
5
+ console.error(
6
+ `Flare JavaScript client v${build.clientVersion}: ${message}`,
7
+ );
8
+ }
9
+
10
+ return !!value;
11
+ }
12
+
13
+ // https://stackoverflow.com/a/11616993/6374824
14
+ export function flatJsonStringify(json: Object): string {
15
+ let cache: any = [];
16
+
17
+ const flattenedStringifiedJson = JSON.stringify(json, function (_, value) {
18
+ if (typeof value === 'object' && value !== null) {
19
+ if (cache.indexOf(value) !== -1) {
20
+ try {
21
+ return JSON.parse(JSON.stringify(value));
22
+ } catch (error) {
23
+ return;
24
+ }
25
+ }
26
+ cache.push(value);
27
+ }
28
+ return value;
29
+ });
30
+
31
+ cache = null;
32
+
33
+ return flattenedStringifiedJson;
34
+ }
35
+
36
+ export function now(): number {
37
+ return Math.round(Date.now() / 1000);
38
+ }
39
+
40
+ export function flattenOnce(array: Array<Array<any>>) {
41
+ return array.reduce((flat, toFlatten) => {
42
+ return flat.concat(toFlatten);
43
+ }, []);
44
+ }
@@ -0,0 +1,50 @@
1
+ // @ts-nocheck
2
+ import { flare } from '../src/index';
3
+ import { expect, test } from 'vitest';
4
+
5
+ test('properly lights', (done) => {
6
+ const projectKey = 'aprojectkey';
7
+
8
+ flare.light(projectKey);
9
+
10
+ expect(flare.config.key).toBe(projectKey);
11
+ });
12
+
13
+ test('can create glows', (done) => {
14
+ flare.glow('glowName', undefined, undefined);
15
+
16
+ expect(flare.glows.length).toBe(1);
17
+
18
+ expect(flare.glows[0]).toMatchObject({
19
+ name: 'glowName',
20
+ message_level: 'info',
21
+ meta_data: [],
22
+ });
23
+
24
+ expect(typeof flare.glows[0].microtime).toBe('number');
25
+ expect(typeof flare.glows[0].time).toBe('number');
26
+ });
27
+
28
+ test.todo('can register solution providers');
29
+
30
+ test.todo('can use solution providers properly');
31
+
32
+ test.todo('can use async solution providers properly');
33
+
34
+ test.todo('can build an error report from an error');
35
+
36
+ test.todo('can create custom context and context groups');
37
+
38
+ test.todo('can throttle when too many reports are being sent');
39
+
40
+ test.todo('can stop a report from being submitted by using beforeSubmit');
41
+
42
+ test.todo('can use an async beforeSubmit');
43
+
44
+ test.todo('can edit a report using beforeSubmit and still send it');
45
+
46
+ test.todo('can stop an error from being evaluated by using beforeEvaluate');
47
+
48
+ test.todo('can use an async beforeEvaluate');
49
+
50
+ test.todo('can check out an error using beforeEvaluate and still send it');
package/tsconfig.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "include": ["src"],
4
+ }