@flareapp/js 1.0.1 → 2.0.0-rc.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/src/Flare.ts ADDED
@@ -0,0 +1,208 @@
1
+ import { Api } from './api';
2
+ import { collectContext } from './context';
3
+ import { CLIENT_VERSION, KEY, SOURCEMAP_VERSION } from './env';
4
+ import { getSolutions } from './solutions';
5
+ import { createStackTrace } from './stacktrace';
6
+ import {
7
+ Config,
8
+ Context,
9
+ Glow,
10
+ MessageLevel,
11
+ Report,
12
+ SolutionProvider,
13
+ SolutionProviderExtraParameters,
14
+ } from './types';
15
+ import { assert, assertKey, assertSolutionProvider, now } from './util';
16
+
17
+ export class Flare {
18
+ config: Config = {
19
+ key: KEY,
20
+ version: CLIENT_VERSION,
21
+ sourcemapVersion: SOURCEMAP_VERSION,
22
+ stage: '',
23
+ maxGlowsPerReport: 30,
24
+ reportingUrl: 'https://reporting.flareapp.io/api/reports',
25
+ reportBrowserExtensionErrors: false,
26
+ debug: false,
27
+ beforeEvaluate: (error) => error,
28
+ beforeSubmit: (report) => report,
29
+ };
30
+
31
+ glows: Glow[] = [];
32
+ context: Context = { context: {} };
33
+ solutionProviders: SolutionProvider[] = [];
34
+
35
+ constructor(public http: Api = new Api()) {}
36
+
37
+ light(key: string = KEY, debug: boolean = false): Flare {
38
+ this.config.key = key;
39
+ this.config.debug = debug;
40
+
41
+ return this;
42
+ }
43
+
44
+ configure(config: Partial<Config>): Flare {
45
+ this.config = { ...this.config, ...config };
46
+
47
+ return this;
48
+ }
49
+
50
+ test(): Promise<void> {
51
+ return this.report(new Error('The Flare client is set up correctly!'));
52
+ }
53
+
54
+ glow(name: string, level: MessageLevel = 'info', data: object | object[] = []): Flare {
55
+ const time = now();
56
+
57
+ this.glows.push({
58
+ name,
59
+ message_level: level,
60
+ meta_data: data,
61
+ time,
62
+ microtime: time,
63
+ });
64
+
65
+ if (this.glows.length > this.config.maxGlowsPerReport) {
66
+ this.glows = this.glows.slice(this.glows.length - this.config.maxGlowsPerReport);
67
+ }
68
+
69
+ return this;
70
+ }
71
+
72
+ clearGlows(): Flare {
73
+ this.glows = [];
74
+
75
+ return this;
76
+ }
77
+
78
+ addContext(name: string, value: any): Flare {
79
+ this.context.context[name] = value;
80
+
81
+ return this;
82
+ }
83
+
84
+ addContextGroup(groupName: string, value: object): Flare {
85
+ this.context[groupName] = value;
86
+
87
+ return this;
88
+ }
89
+
90
+ registerSolutionProvider(solutionProvider: SolutionProvider): Flare {
91
+ if (!assertSolutionProvider(solutionProvider, this.config.debug)) {
92
+ return this;
93
+ }
94
+
95
+ this.solutionProviders.push(solutionProvider);
96
+
97
+ return this;
98
+ }
99
+
100
+ async report(
101
+ error: Error,
102
+ context: Context = {},
103
+ extraSolutionParameters: SolutionProviderExtraParameters = {}
104
+ ): Promise<void> {
105
+ const errorToReport = await this.config.beforeEvaluate(error);
106
+
107
+ if (!errorToReport) {
108
+ return;
109
+ }
110
+
111
+ const report = await this.createReportFromError(error, context, extraSolutionParameters);
112
+
113
+ if (!report) {
114
+ return;
115
+ }
116
+
117
+ return this.sendReport(report);
118
+ }
119
+
120
+ async reportMessage(message: string, context: Context = {}, exceptionClass: string = 'Log'): Promise<void> {
121
+ const stackTrace = await createStackTrace(Error(), this.config.debug);
122
+
123
+ // The first item in the stacktrace is from this file, and irrelevant
124
+ stackTrace.shift();
125
+
126
+ this.sendReport({
127
+ notifier: `Flare JavaScript client v${CLIENT_VERSION}`,
128
+ exception_class: exceptionClass,
129
+ seen_at: now(),
130
+ message: message,
131
+ language: 'javascript',
132
+ glows: this.glows,
133
+ context: collectContext({ ...context, ...this.context }),
134
+ stacktrace: stackTrace,
135
+ sourcemap_version_id: this.config.sourcemapVersion,
136
+ solutions: [],
137
+ stage: this.config.stage,
138
+ });
139
+ }
140
+
141
+ createReportFromError(
142
+ error: Error,
143
+ context: Context = {},
144
+ extraSolutionParameters: SolutionProviderExtraParameters = {}
145
+ ): Promise<Report | false> {
146
+ if (!assert(error, 'No error provided.', this.config.debug)) {
147
+ return Promise.resolve(false);
148
+ }
149
+
150
+ const seenAt = now();
151
+
152
+ return Promise.all([
153
+ getSolutions(this.solutionProviders, error, extraSolutionParameters),
154
+ createStackTrace(error, this.config.debug),
155
+ ]).then((result) => {
156
+ const [solutions, stacktrace] = result;
157
+
158
+ assert(stacktrace.length, "Couldn't generate stacktrace of this error: " + error, this.config.debug);
159
+
160
+ return {
161
+ notifier: `Flare JavaScript client v${CLIENT_VERSION}`,
162
+ exception_class: error.constructor && error.constructor.name ? error.constructor.name : 'undefined',
163
+ seen_at: seenAt,
164
+ message: error.message,
165
+ language: 'javascript',
166
+ glows: this.glows,
167
+ context: collectContext({ ...context, ...this.context }),
168
+ stacktrace,
169
+ sourcemap_version_id: this.config.sourcemapVersion,
170
+ solutions,
171
+ stage: this.config.stage,
172
+ };
173
+ });
174
+ }
175
+
176
+ async sendReport(report: Report): Promise<void> {
177
+ if (!assertKey(this.config.key, this.config.debug)) {
178
+ return;
179
+ }
180
+
181
+ const reportToSubmit = await this.config.beforeSubmit(report);
182
+
183
+ if (!reportToSubmit) {
184
+ return;
185
+ }
186
+
187
+ return this.http.report(
188
+ reportToSubmit,
189
+ this.config.reportingUrl,
190
+ this.config.key,
191
+ this.config.reportBrowserExtensionErrors
192
+ );
193
+ }
194
+
195
+ // Deprecated, the following methods exist for backwards compatibility.
196
+
197
+ set beforeEvaluate(beforeEvaluate: Config['beforeEvaluate']) {
198
+ this.config.beforeEvaluate = beforeEvaluate ?? '';
199
+ }
200
+
201
+ set beforeSubmit(beforeSubmit: Config['beforeSubmit']) {
202
+ this.config.beforeSubmit = beforeSubmit ?? '';
203
+ }
204
+
205
+ set stage(stage: string | undefined) {
206
+ this.config.stage = stage ?? '';
207
+ }
208
+ }
package/src/api/Api.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { Report } from '../types';
2
+ import { flatJsonStringify } from '../util';
3
+
4
+ export class Api {
5
+ report(report: Report, url: string, key: string, reportBrowserExtensionErrors: boolean): Promise<void> {
6
+ return fetch(url, {
7
+ method: 'POST',
8
+ headers: {
9
+ 'Content-Type': 'application/json',
10
+ 'X-Api-Token': key,
11
+ 'X-Requested-With': 'XMLHttpRequest',
12
+ 'X-Report-Browser-Extension-Errors': JSON.stringify(reportBrowserExtensionErrors),
13
+ },
14
+ body: flatJsonStringify({
15
+ ...report,
16
+ key: key,
17
+ }),
18
+ }).then(
19
+ (response) => {
20
+ if (response.status !== 204) {
21
+ console.error(`Received response with status ${response.status} from Flare`);
22
+ }
23
+ },
24
+ (error) => console.error(error)
25
+ );
26
+ }
27
+ }
@@ -0,0 +1 @@
1
+ export * from './Api';
@@ -1,8 +1,9 @@
1
- export default function catchWindowErrors() {
1
+ export function catchWindowErrors() {
2
2
  if (typeof window === 'undefined') {
3
3
  return;
4
4
  }
5
5
 
6
+ // @ts-ignore
6
7
  const flare = window.flare;
7
8
 
8
9
  if (!window || !flare) {
@@ -27,8 +28,6 @@ export default function catchWindowErrors() {
27
28
  flare.report(error.reason);
28
29
  }
29
30
 
30
- // TODO: Maybe also send errors without stacktrace for unhandled rejections without an Error as reason? (could be a string, …)
31
-
32
31
  if (typeof originalOnunhandledrejectionHandler === 'function') {
33
32
  // @ts-ignore
34
33
  originalOnunhandledrejectionHandler(error);
@@ -0,0 +1 @@
1
+ export * from './catchWindowErrors';
@@ -0,0 +1,18 @@
1
+ import { Context } from '../types';
2
+
3
+ import cookie from './cookie';
4
+ import request from './request';
5
+ import requestData from './requestData';
6
+
7
+ export function collectContext(additionalContext: object): Context {
8
+ if (typeof window === 'undefined') {
9
+ return additionalContext;
10
+ }
11
+
12
+ return {
13
+ ...cookie(),
14
+ ...request(),
15
+ ...requestData(),
16
+ ...additionalContext,
17
+ };
18
+ }
@@ -11,7 +11,7 @@ export default function cookie() {
11
11
 
12
12
  return cookies;
13
13
  },
14
- {} as { [key: string]: string },
14
+ {} as { [key: string]: string }
15
15
  ),
16
16
  };
17
17
  }
@@ -1,17 +1 @@
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
- }
1
+ export * from './collectContext';
@@ -0,0 +1,12 @@
1
+ declare const FLARE_JS_KEY: string | undefined;
2
+ declare const FLARE_SOURCEMAP_VERSION: string | undefined;
3
+
4
+ // Injected during build
5
+ export const CLIENT_VERSION =
6
+ typeof process.env.FLARE_JS_CLIENT_VERSION === 'undefined' ? '?' : process.env.FLARE_JS_CLIENT_VERSION;
7
+
8
+ // Injected by flare-vite-plugin-sourcemap-uploader (optional)
9
+ export const KEY = typeof FLARE_JS_KEY === 'undefined' ? '' : FLARE_JS_KEY;
10
+
11
+ // Injected by flare-vite-plugin-sourcemap-uploader (optional)
12
+ export const SOURCEMAP_VERSION = typeof FLARE_SOURCEMAP_VERSION === 'undefined' ? '' : FLARE_SOURCEMAP_VERSION;
package/src/index.ts CHANGED
@@ -1,11 +1,14 @@
1
- import FlareClient from './FlareClient';
2
- import catchWindowErrors from './browserClient';
3
- export { readLinesFromFile } from './stacktrace/fileReader';
1
+ import { Flare } from './Flare';
2
+ import { catchWindowErrors } from './browser';
4
3
 
5
- export const flare = new FlareClient();
4
+ // Expose package singleton
5
+ export const flare = new Flare();
6
6
 
7
7
  if (typeof window !== 'undefined' && window) {
8
+ // @ts-expect-error
8
9
  window.flare = flare;
10
+
11
+ catchWindowErrors();
9
12
  }
10
13
 
11
- catchWindowErrors();
14
+ export { Flare } from './Flare';
@@ -0,0 +1,35 @@
1
+ import { Solution, SolutionProvider, SolutionProviderExtraParameters } from '../types';
2
+ import { flattenOnce } from '../util';
3
+
4
+ export function getSolutions(
5
+ solutionProviders: Array<SolutionProvider>,
6
+ error: Error,
7
+ extraSolutionParameters: SolutionProviderExtraParameters = {}
8
+ ): Promise<Array<Solution>> {
9
+ return new Promise((resolve) => {
10
+ const canSolves = solutionProviders.reduce(
11
+ (canSolves, provider) => {
12
+ canSolves.push(Promise.resolve(provider.canSolve(error, extraSolutionParameters)));
13
+
14
+ return canSolves;
15
+ },
16
+ [] as Array<Promise<boolean>>
17
+ );
18
+
19
+ Promise.all(canSolves).then((resolvedCanSolves) => {
20
+ const solutionPromises: Array<Promise<Array<Solution>>> = [];
21
+
22
+ resolvedCanSolves.forEach((canSolve, i) => {
23
+ if (canSolve) {
24
+ solutionPromises.push(
25
+ Promise.resolve(solutionProviders[i].getSolutions(error, extraSolutionParameters))
26
+ );
27
+ }
28
+ });
29
+
30
+ Promise.all(solutionPromises).then((solutions) => {
31
+ resolve(flattenOnce(solutions));
32
+ });
33
+ });
34
+ });
35
+ }
@@ -1,44 +1 @@
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
- }
1
+ export * from './getSolutions';
@@ -0,0 +1,59 @@
1
+ import ErrorStackParser from 'error-stack-parser';
2
+
3
+ import { StackFrame } from '../types';
4
+ import { assert } from '../util';
5
+
6
+ import { getCodeSnippet } from './fileReader';
7
+
8
+ export function createStackTrace(error: Error, debug: boolean): Promise<Array<StackFrame>> {
9
+ return new Promise((resolve) => {
10
+ if (!hasStack(error)) {
11
+ assert(false, "Couldn't generate stacktrace of below error:", debug);
12
+
13
+ if (debug) {
14
+ console.error(error);
15
+ }
16
+
17
+ return resolve([
18
+ {
19
+ line_number: 0,
20
+ column_number: 0,
21
+ method: 'unknown',
22
+ file: 'unknown',
23
+ code_snippet: {
24
+ 0: 'Could not read from file: stacktrace missing',
25
+ },
26
+ trimmed_column_number: null,
27
+ class: 'unknown',
28
+ },
29
+ ]);
30
+ }
31
+
32
+ Promise.all(
33
+ ErrorStackParser.parse(error).map((frame) => {
34
+ return new Promise<StackFrame>((resolve) => {
35
+ getCodeSnippet(frame.fileName, frame.lineNumber, frame.columnNumber).then((snippet) => {
36
+ resolve({
37
+ line_number: frame.lineNumber || 1,
38
+ column_number: frame.columnNumber || 1,
39
+ method: frame.functionName || 'Anonymous or unknown function',
40
+ file: frame.fileName || 'Unknown file',
41
+ code_snippet: snippet.codeSnippet,
42
+ trimmed_column_number: snippet.trimmedColumnNumber,
43
+ class: '',
44
+ });
45
+ });
46
+ });
47
+ })
48
+ ).then(resolve);
49
+ });
50
+ }
51
+
52
+ function hasStack(err: any): boolean {
53
+ return (
54
+ !!err &&
55
+ (!!err.stack || !!err.stacktrace || !!err['opera#sourceloc']) &&
56
+ typeof (err.stack || err.stacktrace || err['opera#sourceloc']) === 'string' &&
57
+ err.stack !== `${err.name}: ${err.message}`
58
+ );
59
+ }
@@ -9,11 +9,7 @@ type ReaderResponse = {
9
9
  trimmedColumnNumber: number | null;
10
10
  };
11
11
 
12
- export function getCodeSnippet(
13
- url?: string,
14
- lineNumber?: number,
15
- columnNumber?: number,
16
- ): Promise<ReaderResponse> {
12
+ export function getCodeSnippet(url?: string, lineNumber?: number, columnNumber?: number): Promise<ReaderResponse> {
17
13
  return new Promise((resolve) => {
18
14
  if (!url || !lineNumber) {
19
15
  return resolve({
@@ -34,9 +30,7 @@ export function getCodeSnippet(
34
30
  });
35
31
  }
36
32
 
37
- return resolve(
38
- readLinesFromFile(fileText, lineNumber, columnNumber),
39
- );
33
+ return resolve(readLinesFromFile(fileText, lineNumber, columnNumber));
40
34
  });
41
35
  });
42
36
  }
@@ -62,7 +56,7 @@ export function readLinesFromFile(
62
56
  lineNumber: number,
63
57
  columnNumber?: number,
64
58
  maxSnippetLineLength = 1000,
65
- maxSnippetLines = 40,
59
+ maxSnippetLines = 40
66
60
  ): ReaderResponse {
67
61
  const codeSnippet: CodeSnippet = {};
68
62
  let trimmedColumnNumber = null;
@@ -76,28 +70,20 @@ export function readLinesFromFile(
76
70
  const displayLine = currentLineIndex + 1; // the linenumber in a stacktrace is not zero-based like an array
77
71
 
78
72
  if (lines[currentLineIndex].length > maxSnippetLineLength) {
79
- if (
80
- columnNumber &&
81
- columnNumber + maxSnippetLineLength / 2 >
82
- maxSnippetLineLength
83
- ) {
73
+ if (columnNumber && columnNumber + maxSnippetLineLength / 2 > maxSnippetLineLength) {
84
74
  codeSnippet[displayLine] = lines[currentLineIndex].substr(
85
75
  columnNumber - Math.round(maxSnippetLineLength / 2),
86
- maxSnippetLineLength,
76
+ maxSnippetLineLength
87
77
  );
88
78
 
89
79
  if (displayLine === lineNumber) {
90
- trimmedColumnNumber = Math.round(
91
- maxSnippetLineLength / 2,
92
- );
80
+ trimmedColumnNumber = Math.round(maxSnippetLineLength / 2);
93
81
  }
94
82
 
95
83
  continue;
96
84
  }
97
85
 
98
- codeSnippet[displayLine] =
99
- lines[currentLineIndex].substr(0, maxSnippetLineLength) +
100
- '…';
86
+ codeSnippet[displayLine] = lines[currentLineIndex].substr(0, maxSnippetLineLength) + '…';
101
87
 
102
88
  continue;
103
89
  }
@@ -1,71 +1 @@
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
- }
1
+ export * from './createStackTrace';