@servicetitan/log-service 22.14.0 → 22.16.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.
@@ -1 +1 @@
1
- {"version":3,"file":"log-sender.d.ts","sourceRoot":"","sources":["../src/log-sender.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,SAAS,EAAoB,GAAG,EAAE,MAAM,OAAO,CAAC;AACzD,OAAO,EAAE,UAAU,EAAuB,MAAM,eAAe,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAEzD,qBACa,SAAS;IAgBa,QAAQ,CAAC,iBAAiB,EAAE,iBAAiB;IACpD,QAAQ,CAAC,UAAU,EAAE,UAAU;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IACvC,QAAQ,CAAC,GAAG,EAAE,GAAG;IAlBlC,OAAO,CAAC,SAAS,CAAS;IAE1B,OAAO,CAAC,SAAS,CAAO;IAExB,OAAO,CAAC,eAAe,CAAa;IACpC,OAAO,CAAC,mBAAmB,CAAQ;IACnC,OAAO,CAAC,oBAAoB,CAA4B;IACxD,OAAO,CAAC,mBAAmB,CAAkC;IAE7D,OAAO,CAAC,uBAAuB,CAAS;IAExC,OAAO,CAAC,YAAY,CAAC,CAAS;IAC9B,OAAO,CAAC,sBAAsB,CAAC,CAAoB;gBAGX,iBAAiB,EAAE,iBAAiB,EAC3C,UAAU,EAAE,UAAU,EACR,SAAS,EAAE,SAAS,EACzC,GAAG,EAAE,GAAG;IAMlC,OAAO,CAAC,cAAc;IAOtB,OAAO,CAAC,eAAe;YAST,IAAI;IA2ClB,OAAO,CAAC,sBAAsB;IAW9B,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,oBAAoB;CAQ/B"}
1
+ {"version":3,"file":"log-sender.d.ts","sourceRoot":"","sources":["../src/log-sender.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,SAAS,EAAoB,GAAG,EAAE,MAAM,OAAO,CAAC;AACzD,OAAO,EAAE,UAAU,EAAuB,MAAM,eAAe,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAEzD,qBACa,SAAS;IAgBa,QAAQ,CAAC,iBAAiB,EAAE,iBAAiB;IACpD,QAAQ,CAAC,UAAU,EAAE,UAAU;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IACvC,QAAQ,CAAC,GAAG,EAAE,GAAG;IAlBlC,OAAO,CAAC,SAAS,CAAS;IAE1B,OAAO,CAAC,SAAS,CAAO;IAExB,OAAO,CAAC,eAAe,CAAa;IACpC,OAAO,CAAC,mBAAmB,CAAQ;IACnC,OAAO,CAAC,oBAAoB,CAA4B;IACxD,OAAO,CAAC,mBAAmB,CAAkC;IAE7D,OAAO,CAAC,uBAAuB,CAAS;IAExC,OAAO,CAAC,YAAY,CAAC,CAAS;IAC9B,OAAO,CAAC,sBAAsB,CAAC,CAAoB;gBAGX,iBAAiB,EAAE,iBAAiB,EAC3C,UAAU,EAAE,UAAU,EACR,SAAS,EAAE,SAAS,EACzC,GAAG,EAAE,GAAG;IAMlC,OAAO,CAAC,cAAc;IAOtB,OAAO,CAAC,eAAe;YAST,IAAI;IAgDlB,OAAO,CAAC,sBAAsB;IAW9B,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,oBAAoB;CAQ/B"}
@@ -132,12 +132,14 @@ let LogSender = class LogSender {
132
132
  });
133
133
  return;
134
134
  }
135
+ /* istanbul ignore next */
135
136
  if (!this.connectionService.online) {
136
137
  throw new Error('cannot_send_while_offline');
137
138
  }
138
139
  this.isSending = true;
139
140
  try {
140
141
  const logEvents = this.logStorage.loadBatch(this.batchSize);
142
+ /* istanbul ignore next */
141
143
  if (!logEvents.length) {
142
144
  throw new Error('zero_log_events');
143
145
  }
@@ -155,7 +157,10 @@ let LogSender = class LogSender {
155
157
  else {
156
158
  this.increaseSendInterval();
157
159
  }
158
- throw e;
160
+ /* istanbul ignore next */
161
+ if (process.env.NODE_ENV !== 'test') {
162
+ throw e;
163
+ }
159
164
  }
160
165
  }
161
166
  finally {
@@ -1 +1 @@
1
- {"version":3,"file":"log-sender.js","sourceRoot":"","sources":["../src/log-sender.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"log-sender.js","sourceRoot":"","sources":["../src/log-sender.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@servicetitan/log-service",
3
- "version": "22.14.0",
3
+ "version": "22.16.0",
4
4
  "description": "",
5
5
  "homepage": "https://docs.st.dev/docs/frontend/log-service",
6
6
  "repository": {
@@ -16,14 +16,15 @@
16
16
  "src"
17
17
  ],
18
18
  "devDependencies": {
19
- "@servicetitan/react-ioc": "22.14.0",
19
+ "@servicetitan/react-ioc": "22.16.0",
20
+ "@testing-library/react": "^12.1.5",
20
21
  "@types/lodash.throttle": "~4.1.6",
21
22
  "axios": "~0.27.2",
22
23
  "mobx": "~6.10.0",
23
24
  "react": "~17.0.2"
24
25
  },
25
26
  "peerDependencies": {
26
- "@servicetitan/react-ioc": "22.14.0",
27
+ "@servicetitan/react-ioc": "22.16.0",
27
28
  "axios": "~0.27.2",
28
29
  "mobx": "~6.10.0",
29
30
  "react": "~17.0.2"
@@ -37,5 +38,5 @@
37
38
  "cli": {
38
39
  "webpack": false
39
40
  },
40
- "gitHead": "e0064d9f47c180d6b128f1b19f92c5f46d4e0320"
41
+ "gitHead": "79adb1a1ec5d89601541349b01201a16bc2e2db9"
41
42
  }
@@ -0,0 +1,56 @@
1
+ import { ConnectionService } from '../connection-service';
2
+
3
+ describe(`[log-service] ${ConnectionService.name}`, () => {
4
+ type Mutable<T> = { -readonly [P in keyof T]: T[P] };
5
+ const navigator = window.navigator as Mutable<typeof window.navigator>;
6
+
7
+ beforeAll(() => {
8
+ Object.defineProperty(window.navigator, 'onLine', { value: undefined, writable: true });
9
+ });
10
+
11
+ const subject = () => new ConnectionService();
12
+
13
+ describe('when browser is offline', () => {
14
+ beforeEach(() => (navigator.onLine = false));
15
+
16
+ test('reports offline', () => {
17
+ expect(subject().online).toBe(false);
18
+ });
19
+
20
+ describe('when browser goes online', () => {
21
+ const setup = () => {
22
+ const service = subject();
23
+ window.dispatchEvent(new Event('online'));
24
+ return service;
25
+ };
26
+
27
+ test('reports online', () => {
28
+ const service = setup();
29
+
30
+ expect(service.online).toBe(true);
31
+ });
32
+ });
33
+ });
34
+
35
+ describe('when browser is online', () => {
36
+ beforeEach(() => (navigator.onLine = true));
37
+
38
+ test('reports online', () => {
39
+ expect(subject().online).toBe(true);
40
+ });
41
+
42
+ describe('when browser goes offline', () => {
43
+ const setup = () => {
44
+ const service = subject();
45
+ window.dispatchEvent(new Event('offline'));
46
+ return service;
47
+ };
48
+
49
+ test('reports offline', () => {
50
+ const service = setup();
51
+
52
+ expect(service.online).toBe(false);
53
+ });
54
+ });
55
+ });
56
+ });
@@ -0,0 +1,243 @@
1
+ import { Log } from '../log';
2
+ import { serializeWithMetadata } from '../serialize-with-metadata';
3
+ import { GlobalErrorHandler } from '../global-error-handler';
4
+
5
+ describe(`[log-service] ${GlobalErrorHandler.name}`, () => {
6
+ const log: Pick<Log, 'error'> = { error: jest.fn() };
7
+ let handler: GlobalErrorHandler;
8
+
9
+ beforeAll(() => (handler = new GlobalErrorHandler(log as Log)));
10
+
11
+ beforeEach(() => jest.clearAllMocks());
12
+
13
+ describe('onError', () => {
14
+ const line = 12;
15
+ const column = 34;
16
+ const url = 'http://example.com';
17
+ let message: string;
18
+ let error: Error | undefined;
19
+ let isUnhandled: boolean | undefined;
20
+
21
+ beforeEach(() => {
22
+ message = 'Oops!';
23
+ error = new Error('Something went wrong');
24
+ isUnhandled = undefined;
25
+ });
26
+
27
+ const subject = () => handler.onError(message, url, line, column, error, isUnhandled);
28
+
29
+ test('reports error', () => {
30
+ subject();
31
+
32
+ expect(log.error).toHaveBeenCalledWith({
33
+ error,
34
+ message: error!.message,
35
+ category: 'UnhandledError',
36
+ code: 'UnhandledError',
37
+ data: undefined,
38
+ });
39
+ });
40
+
41
+ describe('when error was handled', () => {
42
+ beforeEach(() => (isUnhandled = false));
43
+
44
+ test('omits category and code', () => {
45
+ subject();
46
+
47
+ expect(log.error).toHaveBeenCalledWith({
48
+ error,
49
+ message: error!.message,
50
+ });
51
+ });
52
+ });
53
+
54
+ describe('with no error', () => {
55
+ beforeEach(() => (error = undefined));
56
+
57
+ test('reports synthetic error', () => {
58
+ subject();
59
+
60
+ expect(log.error).toHaveBeenCalledWith({
61
+ error: expect.objectContaining({
62
+ message: `Message - ${message}`,
63
+ stack: expect.stringContaining(`${url}:${line}:${column}`),
64
+ }),
65
+ message: `Message - ${message}`,
66
+ category: 'UnhandledError',
67
+ code: 'NonUniform',
68
+ });
69
+ });
70
+
71
+ describe('when message is not a string', () => {
72
+ beforeEach(() => ((message as any) = { foo: 'bar' }));
73
+
74
+ test('serializes message', () => {
75
+ subject();
76
+
77
+ expect(log.error).toHaveBeenCalledWith(
78
+ expect.objectContaining({
79
+ message: `Message - ${serializeWithMetadata(message)}`,
80
+ })
81
+ );
82
+ });
83
+ });
84
+ });
85
+
86
+ describe('with a non-standard error', () => {
87
+ beforeEach(() => ((error as any) = { data: 'foo' }));
88
+
89
+ test('serializes error into message', () => {
90
+ subject();
91
+
92
+ expect(log.error).toHaveBeenCalledWith(
93
+ expect.objectContaining({
94
+ message: `Message - ${message};Error - ${serializeWithMetadata(error)}`,
95
+ })
96
+ );
97
+ });
98
+ });
99
+
100
+ describe.each([
101
+ 'gCrWeb',
102
+ 'Geolocation',
103
+ 'kCLErrorDomain',
104
+ 'The last location provider was disabled',
105
+ 'Location services are disabled',
106
+ ])('when message contains "%s"', subString => {
107
+ beforeEach(() => (message = `foo${subString}bar`));
108
+
109
+ test('ignores error', () => {
110
+ subject();
111
+
112
+ expect(log.error).not.toHaveBeenCalled();
113
+ });
114
+ });
115
+
116
+ describe('when report fails', () => {
117
+ const reportError = new Error('Something went wrong');
118
+
119
+ beforeEach(() => {
120
+ jest.mocked(log.error).mockImplementation(() => {
121
+ throw reportError;
122
+ });
123
+ });
124
+
125
+ test('logs failure', () => {
126
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn());
127
+
128
+ subject();
129
+
130
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
131
+ '@log-service/onError/failsafe',
132
+ message
133
+ );
134
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
135
+ '@log-service/onError/failsafe',
136
+ reportError
137
+ );
138
+ });
139
+ });
140
+ });
141
+
142
+ describe('onReject', () => {
143
+ let reason: any;
144
+
145
+ const subject = () => handler.onReject({ reason });
146
+
147
+ describe('when reason is an Error', () => {
148
+ const error = new Error('Oops!');
149
+
150
+ beforeEach(() => (reason = error));
151
+
152
+ test('reports error', () => {
153
+ subject();
154
+
155
+ expect(log.error).toHaveBeenCalledWith({
156
+ error,
157
+ message: error.message,
158
+ });
159
+ });
160
+ });
161
+
162
+ describe('when reason is not an error', () => {
163
+ beforeEach(() => (reason = { foo: 'bar' }));
164
+
165
+ test('reports synthetic error', () => {
166
+ subject();
167
+
168
+ const message = `Unhandled rejection - ${serializeWithMetadata(reason)}`;
169
+ expect(log.error).toHaveBeenCalledWith({
170
+ error: expect.objectContaining({ message }),
171
+ message,
172
+ code: 'NonUniform',
173
+ });
174
+ });
175
+ });
176
+
177
+ describe('when report fails', () => {
178
+ const reportError = new Error('Something went wrong');
179
+
180
+ beforeEach(() => {
181
+ jest.mocked(log.error).mockImplementation(() => {
182
+ throw reportError;
183
+ });
184
+ });
185
+
186
+ test('logs failure', () => {
187
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn());
188
+
189
+ subject();
190
+
191
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
192
+ '@log-service/onReject/failsafe',
193
+ reportError
194
+ );
195
+ });
196
+ });
197
+ });
198
+
199
+ describe('instrument', () => {
200
+ let window: { onerror?: any; onunhandledrejection?: any };
201
+
202
+ beforeEach(() => (window = {}));
203
+
204
+ const subject = () => handler.instrument(window as any);
205
+
206
+ test('installs "onError" handler', () => {
207
+ subject();
208
+
209
+ expect(window.onerror).toBe(handler.onError);
210
+ });
211
+
212
+ test('installs "onReject" handler', () => {
213
+ subject();
214
+
215
+ expect(window.onunhandledrejection).toBe(handler.onReject);
216
+ });
217
+
218
+ describe('with existing handler', () => {
219
+ const existingHandler = jest.fn();
220
+
221
+ beforeEach(() => (window.onerror = existingHandler));
222
+
223
+ test('chains the error handlers', () => {
224
+ const onErrorSpy = jest.spyOn(handler, 'onError');
225
+ const error = new Error('Oops!');
226
+
227
+ subject();
228
+ window.onerror(error);
229
+
230
+ expect(existingHandler).toHaveBeenCalledWith(error);
231
+ expect(onErrorSpy).toHaveBeenCalledWith(error);
232
+ });
233
+
234
+ describe('when existing error handler is not a function', () => {
235
+ beforeEach(() => (window.onerror = 'Nope!'));
236
+
237
+ test('throws error', () => {
238
+ expect(subject).toThrowError(/attempt to instrument non-function value/i);
239
+ });
240
+ });
241
+ });
242
+ });
243
+ });
@@ -0,0 +1,29 @@
1
+ import { useDependencies } from '@servicetitan/react-ioc';
2
+ import { render } from '@testing-library/react';
3
+ import { FC } from 'react';
4
+
5
+ import { LOG_CONFIG_TOKEN, LogConfig, LogConfigProvider } from '../log';
6
+
7
+ describe(`[log-service] ${LogConfigProvider.name}`, () => {
8
+ const logConfig: LogConfig = { endpoint: 'https//www.example.com/foo' };
9
+ let providedLogConfig: LogConfig;
10
+
11
+ const Wrapper: FC = () => {
12
+ const [logConfig] = useDependencies(LOG_CONFIG_TOKEN);
13
+ providedLogConfig = logConfig;
14
+ return null;
15
+ };
16
+
17
+ const subject = () =>
18
+ render(
19
+ <LogConfigProvider value={logConfig}>
20
+ <Wrapper />
21
+ </LogConfigProvider>
22
+ );
23
+
24
+ test('provides LOG_CONFIG_TOKEN', () => {
25
+ subject();
26
+
27
+ expect(providedLogConfig).toBe(logConfig);
28
+ });
29
+ });
@@ -0,0 +1,137 @@
1
+ import axios from 'axios';
2
+ import { ConnectionService } from '../connection-service';
3
+ import { Log, LogConfig, LogEntryType, LogInfo, LogWarning } from '../log';
4
+ import { LogStorage } from '../log-storage';
5
+
6
+ import { LogSender } from '../log-sender';
7
+
8
+ jest.mock('axios', () => ({ post: jest.fn() }));
9
+
10
+ class MockLog {
11
+ constructor(private readonly logStorage: LogStorage) {}
12
+
13
+ info(entry: LogInfo) {
14
+ this.logStorage.store({
15
+ type: LogEntryType.Info,
16
+ ...entry,
17
+ category: null,
18
+ code: null,
19
+ data: {},
20
+ error: null,
21
+ message: null,
22
+ });
23
+ }
24
+
25
+ warning(_entry: LogWarning) {}
26
+ }
27
+
28
+ const SECOND = 1000;
29
+ const MINUTE = 60 * SECOND;
30
+
31
+ describe(`[log-service] ${LogSender.name}`, () => {
32
+ const connectionService = new ConnectionService();
33
+ const logConfig: LogConfig = { endpoint: 'https://www.example.com' };
34
+ let logStorage: LogStorage;
35
+ let log: Pick<Log, 'info' | 'warning'>;
36
+
37
+ beforeAll(() => {
38
+ jest.spyOn(window, 'setInterval').mockImplementation(jest.fn());
39
+ process.env.NODE_ENV ??= 'test'; // workaround issue with Jest not setting NODE_ENV=test
40
+ });
41
+
42
+ beforeEach(() => {
43
+ jest.clearAllMocks();
44
+ jest.spyOn(axios, 'post').mockImplementation(jest.fn());
45
+
46
+ logStorage = new LogStorage();
47
+ log = new MockLog(logStorage);
48
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
49
+ const logSender = new LogSender(connectionService, logStorage, logConfig, log as Log);
50
+ });
51
+
52
+ const subject = async () => {
53
+ getHandler().fn();
54
+ await new Promise(process.nextTick); // resolve pending promises
55
+ };
56
+
57
+ function getHandler() {
58
+ const [fn, interval] = jest.mocked(window.setInterval).mock.calls[0];
59
+ return { fn, interval };
60
+ }
61
+
62
+ function initLog(count: number) {
63
+ Array.from({ length: count }).map((_, index) => log.info({ message: `Message ${index}` }));
64
+ return logStorage.items;
65
+ }
66
+
67
+ test('schedules handler to run once per minute', async () => {
68
+ await subject();
69
+
70
+ expect(getHandler().interval).toBe(MINUTE);
71
+ });
72
+
73
+ describe('when there are items waiting to be sent', () => {
74
+ beforeEach(() => initLog(3));
75
+
76
+ test('handler posts then purges log', async () => {
77
+ const { items } = logStorage;
78
+
79
+ await subject();
80
+
81
+ expect(axios.post).toHaveBeenCalledWith(logConfig.endpoint, items);
82
+ expect(logStorage.items).toEqual([]);
83
+ });
84
+
85
+ describe('when post fails', () => {
86
+ beforeEach(() => jest.mocked(axios.post).mockRejectedValue(new Error('Oops!')));
87
+
88
+ test('reschedules handler to run in exponentially increasing intervals until it succeeds', async () => {
89
+ for (let n = 0; n <= 10; n++) {
90
+ // eslint-disable-next-line no-await-in-loop
91
+ await subject();
92
+ }
93
+
94
+ for (let n = 0; n < 10; n++) {
95
+ expect(window.setInterval).toHaveBeenCalledWith(
96
+ expect.any(Function),
97
+ 2 ** n * SECOND
98
+ );
99
+ }
100
+
101
+ jest.mocked(axios.post).mockReset();
102
+ await subject();
103
+
104
+ expect(logStorage.items).toEqual([]);
105
+ expect(window.setInterval).toHaveBeenCalledWith(expect.any(Function), MINUTE);
106
+ });
107
+ });
108
+
109
+ describe('when handler runs while posting', () => {
110
+ beforeEach(() =>
111
+ jest.spyOn(axios, 'post').mockImplementation(() => {
112
+ getHandler().fn();
113
+ return Promise.resolve({});
114
+ })
115
+ );
116
+
117
+ test('logs warning', async () => {
118
+ const spy = jest.spyOn(log, 'warning');
119
+
120
+ await subject();
121
+
122
+ expect(spy).toHaveBeenCalledWith(
123
+ expect.objectContaining({ message: 'send_already_in_progress' })
124
+ );
125
+ });
126
+ });
127
+ });
128
+
129
+ test('automatically posts when log >= 128 items', async () => {
130
+ await subject();
131
+ expect(axios.post).not.toHaveBeenCalled();
132
+
133
+ const items = initLog(128);
134
+
135
+ expect(axios.post).toHaveBeenCalledWith(logConfig.endpoint, items);
136
+ });
137
+ });
@@ -0,0 +1,150 @@
1
+ import { EntryComplete, LogEntryType } from '../log';
2
+ import { LogStorage } from '../log-storage';
3
+
4
+ describe(`${LogStorage.name}`, () => {
5
+ const LOCAL_STORAGE_KEY = 'LogStorage';
6
+ const LOCAL_STORAGE_VERSION = '1';
7
+ const LOCAL_STORAGE_VERSION_KEY = 'LogStorageVersion';
8
+
9
+ beforeEach(() => window.localStorage.clear());
10
+
11
+ const subject = (entryOrEntries: EntryComplete | EntryComplete[] = []) => {
12
+ const entries = Array.isArray(entryOrEntries) ? entryOrEntries : [entryOrEntries];
13
+ const logStorage = new LogStorage();
14
+ entries.forEach(entry => logStorage.store(entry));
15
+ return logStorage;
16
+ };
17
+
18
+ function itLoadsNoItems() {
19
+ test('loads no items', () => {
20
+ expect(subject().items).toEqual([]);
21
+ });
22
+ }
23
+
24
+ itLoadsNoItems();
25
+
26
+ describe('with previously saved items', () => {
27
+ const entries = createEntries(3);
28
+
29
+ beforeEach(() => {
30
+ const logStorage = new LogStorage();
31
+ entries.map(entry => logStorage.store(entry));
32
+ });
33
+
34
+ function itLoadsSavedItems() {
35
+ test('loads saved items', () => {
36
+ expect(subject().items).toEqual(buildItems(entries).map(expect.objectContaining));
37
+ });
38
+ }
39
+
40
+ itLoadsSavedItems();
41
+
42
+ describe('when saved data has incompatible version', () => {
43
+ beforeEach(() => window.localStorage.setItem(LOCAL_STORAGE_VERSION_KEY, '-1'));
44
+
45
+ itLoadsNoItems();
46
+
47
+ test('updates version', () => {
48
+ subject();
49
+
50
+ expect(localStorage.getItem(LOCAL_STORAGE_VERSION_KEY)).toBe(LOCAL_STORAGE_VERSION);
51
+ });
52
+ });
53
+
54
+ describe('when saved data is not valid', () => {
55
+ beforeEach(() => {
56
+ window.localStorage.setItem(LOCAL_STORAGE_KEY, '}{');
57
+ });
58
+
59
+ itLoadsNoItems();
60
+ });
61
+
62
+ describe('when saved data contains old "error" field', () => {
63
+ beforeEach(() => {
64
+ const itemsWithErrorField = (
65
+ JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)!) as LogStorage['items']
66
+ ).map(({ exception, ...rest }) => ({ ...rest, error: exception }));
67
+ localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(itemsWithErrorField));
68
+ });
69
+
70
+ itLoadsSavedItems();
71
+ });
72
+ });
73
+
74
+ test('stores items with unique ids', () => {
75
+ const entries = createEntries(3);
76
+
77
+ const logStorage = subject(entries);
78
+
79
+ expect(logStorage.items).toEqual(buildItems(entries).map(expect.objectContaining));
80
+ expect(new Set(logStorage.items.map(({ id }) => id)).size).toBe(entries.length);
81
+ });
82
+
83
+ test('limits storage to 1000 items', () => {
84
+ const logStorage = subject(createEntries(1001));
85
+
86
+ expect(logStorage.items).toHaveLength(1000);
87
+ });
88
+
89
+ test('stringifies entry data', () => {
90
+ const entry = createEntry({ message: 'foo', data: { foo: 'bar' } });
91
+
92
+ const { data } = subject(entry).items[0];
93
+
94
+ expect(data).toBe(JSON.stringify(entry.data));
95
+ });
96
+
97
+ test('converts entry error to exception', () => {
98
+ const error = new Error('Oops!');
99
+
100
+ const { exception } = subject(createEntry({ error })).items[0];
101
+
102
+ expect(exception).toEqual(JSON.stringify({ message: error.message, stack: error.stack }));
103
+ });
104
+
105
+ test('deleteToId removes entries with same or lower id', () => {
106
+ const entries = createEntries(10);
107
+ const logStorage = subject(entries);
108
+
109
+ const index = 2;
110
+ const { id } = logStorage.items[index];
111
+ logStorage.deleteToId(id);
112
+
113
+ expect(logStorage.items.length).toBe(entries.length - index - 1);
114
+ expect(logStorage.items.every(entry => entry.id > id)).toBe(true);
115
+ });
116
+
117
+ test('loadBatch returns first N items', () => {
118
+ const entries = createEntries(10);
119
+ const logStorage = subject(entries);
120
+
121
+ const count = 5;
122
+ const batch = logStorage.loadBatch(count);
123
+
124
+ expect(batch).toEqual(buildItems(entries.slice(0, count)).map(expect.objectContaining));
125
+ });
126
+ });
127
+
128
+ function buildItems(entries: EntryComplete[]) {
129
+ return entries.map(({ data, error, ...rest }) => ({
130
+ ...rest,
131
+ data: JSON.stringify(data),
132
+ exception: JSON.stringify(error && { message: error.message, stack: error.stack }),
133
+ }));
134
+ }
135
+
136
+ function createEntries(length: number) {
137
+ return Array.from({ length }).map((_, index) => createEntry({ message: `Entry ${index}` }));
138
+ }
139
+
140
+ function createEntry(entry: Partial<EntryComplete>): EntryComplete {
141
+ return {
142
+ type: LogEntryType.Info,
143
+ message: null,
144
+ code: null,
145
+ category: null,
146
+ error: null,
147
+ data: {},
148
+ ...entry,
149
+ };
150
+ }
@@ -0,0 +1,157 @@
1
+ import { Log, LogEntry, LogEntryType } from '../log';
2
+ import { LogStorage } from '../log-storage';
3
+ import { serializeWithMetadata } from '../serialize-with-metadata';
4
+
5
+ const SECOND = 1000;
6
+
7
+ describe(`[log-service] ${Log.name}`, () => {
8
+ const entry: LogEntry = { category: 'category', code: 'code', message: 'foo' };
9
+
10
+ beforeAll(() => jest.useFakeTimers());
11
+
12
+ beforeEach(() => {
13
+ jest.clearAllTimers();
14
+ jest.spyOn(console, 'error').mockImplementation(jest.fn());
15
+ window.localStorage.clear();
16
+ });
17
+
18
+ const subject = () => {
19
+ const logStorage = new LogStorage();
20
+ const log = new Log(logStorage);
21
+ jest.spyOn(logStorage, 'store');
22
+ return { log, logStorage };
23
+ };
24
+
25
+ test('logs info', () => {
26
+ const { log, logStorage } = subject();
27
+
28
+ log.info(entry);
29
+
30
+ expect(logStorage.store).toHaveBeenCalledWith(
31
+ expect.objectContaining({ ...entry, type: LogEntryType.Info })
32
+ );
33
+ });
34
+
35
+ test('logs warnings', () => {
36
+ const { log, logStorage } = subject();
37
+
38
+ log.warning(entry);
39
+
40
+ expect(logStorage.store).toHaveBeenCalledWith(
41
+ expect.objectContaining({ ...entry, type: LogEntryType.Warning })
42
+ );
43
+ });
44
+
45
+ test('log errors, after delay', () => {
46
+ const errorEntry = { ...entry, error: new Error('Oops!'), message: undefined };
47
+ const { log, logStorage } = subject();
48
+
49
+ log.error(errorEntry);
50
+ jest.runOnlyPendingTimers();
51
+
52
+ expect(logStorage.store).toHaveBeenCalledWith(
53
+ expect.objectContaining({
54
+ ...errorEntry,
55
+ type: LogEntryType.Error,
56
+ message: errorEntry.error.message,
57
+ })
58
+ );
59
+ // eslint-disable-next-line no-console
60
+ expect(console.error).toHaveBeenCalledWith('@log-service/error', errorEntry);
61
+ });
62
+
63
+ test('skips redundant errors', () => {
64
+ const errorEntry = { ...entry, error: new Error('Oops!') };
65
+ const THROTTLE_INTERVAL = 30 * SECOND;
66
+ const { log, logStorage } = subject();
67
+
68
+ Array.from({ length: 5 }).forEach(() => log.error(errorEntry));
69
+ jest.advanceTimersByTime(THROTTLE_INTERVAL - 1);
70
+ Array.from({ length: 5 }).forEach(() => log.error(errorEntry));
71
+
72
+ // Note: 2x because current implementation immediately flushes first entry
73
+ expect(logStorage.store).toHaveBeenCalledTimes(2);
74
+ });
75
+
76
+ test('serializes non-standard errors', () => {
77
+ const errorEntry = { ...entry, error: { foo: 'bar' } };
78
+ const { log, logStorage } = subject();
79
+
80
+ log.error(errorEntry);
81
+ jest.runOnlyPendingTimers();
82
+
83
+ expect(logStorage.store).toHaveBeenCalledWith(
84
+ expect.objectContaining({
85
+ message: errorEntry.message,
86
+ error: expect.objectContaining({
87
+ message: serializeWithMetadata(errorEntry.error),
88
+ }),
89
+ })
90
+ );
91
+ });
92
+
93
+ test('extends log data', () => {
94
+ const data = { foo: 'bar' };
95
+ const { log, logStorage } = subject();
96
+
97
+ log.info({ ...entry, data });
98
+
99
+ expect(logStorage.store).toHaveBeenCalledWith(
100
+ expect.objectContaining({
101
+ data: {
102
+ ...data,
103
+ CreatedOn: new Date(),
104
+ Online: window.navigator.onLine,
105
+ Url: window.location.toString(),
106
+ UserAgent: window.navigator.userAgent,
107
+ Source: 'Frontend',
108
+ Plugins: '',
109
+ TzOffset: expect.any(Number),
110
+ IsDst: expect.any(Boolean),
111
+ IsDstSubject: expect.any(Boolean),
112
+ },
113
+ })
114
+ );
115
+ });
116
+
117
+ describe('when browser has plugins', () => {
118
+ const plugins: Partial<typeof window.navigator.plugins[0]>[] = [
119
+ { name: 'foo', filename: 'filename', description: 'description' },
120
+ { name: 'bar', filename: 'filename', description: 'description' },
121
+ ];
122
+
123
+ beforeEach(() => {
124
+ Object.defineProperty(window.navigator, 'plugins', { value: plugins, writable: true });
125
+ });
126
+
127
+ test('adds plugins to log data', () => {
128
+ const { log, logStorage } = subject();
129
+
130
+ log.info(entry);
131
+
132
+ expect(logStorage.store).toHaveBeenCalledWith(
133
+ expect.objectContaining({
134
+ data: expect.objectContaining({
135
+ Plugins: plugins.map(p => [p.name, p.filename, p.description]).join('/'),
136
+ }),
137
+ })
138
+ );
139
+ });
140
+
141
+ describe('when error occurs reading plugins', () => {
142
+ beforeEach(() => Object.assign(window.navigator, { plugins: [undefined] }));
143
+
144
+ test('reports "unknown"', () => {
145
+ const { log, logStorage } = subject();
146
+
147
+ log.info(entry);
148
+
149
+ expect(logStorage.store).toHaveBeenCalledWith(
150
+ expect.objectContaining({
151
+ data: expect.objectContaining({ Plugins: 'unknown' }),
152
+ })
153
+ );
154
+ });
155
+ });
156
+ });
157
+ });
package/src/log-sender.ts CHANGED
@@ -58,6 +58,7 @@ export class LogSender {
58
58
  });
59
59
  return;
60
60
  }
61
+ /* istanbul ignore next */
61
62
  if (!this.connectionService.online) {
62
63
  throw new Error('cannot_send_while_offline');
63
64
  }
@@ -67,6 +68,7 @@ export class LogSender {
67
68
  try {
68
69
  const logEvents = this.logStorage.loadBatch(this.batchSize);
69
70
 
71
+ /* istanbul ignore next */
70
72
  if (!logEvents.length) {
71
73
  throw new Error('zero_log_events');
72
74
  }
@@ -85,7 +87,10 @@ export class LogSender {
85
87
  this.increaseSendInterval();
86
88
  }
87
89
 
88
- throw e;
90
+ /* istanbul ignore next */
91
+ if (process.env.NODE_ENV !== 'test') {
92
+ throw e;
93
+ }
89
94
  }
90
95
  } finally {
91
96
  this.isSending = false;