Skip to content

Commit 49b32d8

Browse files
authored
[O11Y-201] Prevent log spam for storage methods and check if storage is defined. (#75)
## Summary Adding the ability to log once to prevent log spam. Adding persistent storage availability checks. ## How did you test this change? <!-- Frontend - Leave a screencast or a screenshot to visually describe the changes. --> ## Are there any deployment considerations? <!-- Backend - Do we need to consider migrations or backfilling data? --> ## Does this work require review from our design team? <!-- Request review from julian-highlight / our design team -->
1 parent 254981d commit 49b32d8

File tree

4 files changed

+191
-7
lines changed

4 files changed

+191
-7
lines changed

.changeset/thirty-jeans-exist.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'highlight.run': patch
3+
---
4+
5+
Improve logging for persistent storage issues.

sdk/highlight-run/src/__tests__/utils.test.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import {
33
shouldNetworkRequestBeRecorded,
44
shouldNetworkRequestBeTraced,
55
} from '../client/listeners/network-listener/utils/utils'
6+
import { internalLogOnce } from '../sdk/util'
7+
8+
type TestLogLevel = 'debug' | 'info' | 'error' | 'warn'
69

710
describe('normalizeUrl', () => {
811
vi.stubGlobal('location', {
@@ -264,3 +267,123 @@ describe('shouldNetworkRequestBeRecorded', () => {
264267
).toBe(false)
265268
})
266269
})
270+
271+
describe('internalLogOnce', () => {
272+
let consoleWarnSpy: any
273+
let consoleInfoSpy: any
274+
let consoleErrorSpy: any
275+
let consoleDebugSpy: any
276+
let spyByLevel: Record<TestLogLevel, any>
277+
278+
beforeEach(() => {
279+
consoleDebugSpy = vi
280+
.spyOn(console, 'debug')
281+
.mockImplementation(() => {})
282+
consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation(() => {})
283+
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
284+
consoleErrorSpy = vi
285+
.spyOn(console, 'error')
286+
.mockImplementation(() => {})
287+
spyByLevel = {
288+
debug: consoleDebugSpy,
289+
info: consoleInfoSpy,
290+
warn: consoleWarnSpy,
291+
error: consoleErrorSpy,
292+
}
293+
})
294+
295+
afterEach(() => {
296+
vi.restoreAllMocks()
297+
vi.clearAllMocks()
298+
})
299+
300+
it('logs a message only once for the same context and logOnceId', () => {
301+
const context = 'test-context-1'
302+
const logOnceId = 'test-id'
303+
const message = 'test message'
304+
305+
// First call should log
306+
internalLogOnce(context, logOnceId, 'warn', message)
307+
expect(consoleWarnSpy).toHaveBeenCalledTimes(1)
308+
expect(consoleWarnSpy).toHaveBeenCalledWith(
309+
'[@launchdarkly plugins]: (test-context-1): ',
310+
message,
311+
)
312+
313+
// Second call with same context and logOnceId should not log
314+
internalLogOnce(context, logOnceId, 'warn', message)
315+
expect(consoleWarnSpy).toHaveBeenCalledTimes(1) // Still only called once
316+
317+
// Third call with same context and logOnceId should not log
318+
internalLogOnce(context, logOnceId, 'warn', 'different message')
319+
expect(consoleWarnSpy).toHaveBeenCalledTimes(1) // Still only called once
320+
})
321+
322+
it('allows logging the same logOnceId in different contexts', () => {
323+
const logOnceId = 'shared-id'
324+
const context1 = 'test-context-2'
325+
const context2 = 'test-context-3'
326+
const message = 'test message'
327+
328+
// First call with context-1 should log
329+
internalLogOnce(context1, logOnceId, 'warn', message)
330+
expect(consoleWarnSpy).toHaveBeenCalledTimes(1)
331+
expect(consoleWarnSpy).toHaveBeenLastCalledWith(
332+
'[@launchdarkly plugins]: (test-context-2): ',
333+
message,
334+
)
335+
336+
// Call with context-2 and same logOnceId should also log
337+
internalLogOnce(context2, logOnceId, 'warn', message)
338+
expect(consoleWarnSpy).toHaveBeenCalledTimes(2)
339+
expect(consoleWarnSpy).toHaveBeenLastCalledWith(
340+
'[@launchdarkly plugins]: (test-context-3): ',
341+
message,
342+
)
343+
344+
// Second call with context-1 should not log
345+
internalLogOnce(context1, logOnceId, 'warn', message)
346+
expect(consoleWarnSpy).toHaveBeenCalledTimes(2) // Still only 2 calls
347+
348+
// Second call with context-2 should not log
349+
internalLogOnce(context2, logOnceId, 'warn', message)
350+
expect(consoleWarnSpy).toHaveBeenCalledTimes(2) // Still only 2 calls
351+
})
352+
353+
it.each(['debug', 'info', 'error', 'warn'])(
354+
'works with different log levels',
355+
(level) => {
356+
const context = 'test-context-4'
357+
const logOnceId = `test-id-${level}`
358+
const message = 'test message'
359+
360+
// Test with 'info' level
361+
internalLogOnce(context, logOnceId, level as keyof Console, message)
362+
expect(spyByLevel[level as TestLogLevel]).toHaveBeenCalledTimes(1)
363+
expect(spyByLevel[level as TestLogLevel]).toHaveBeenCalledWith(
364+
'[@launchdarkly plugins]: (test-context-4): ',
365+
message,
366+
)
367+
},
368+
)
369+
370+
it('handles multiple arguments correctly', () => {
371+
const context = 'test-context-5'
372+
const logOnceId = 'test-id'
373+
374+
internalLogOnce(context, logOnceId, 'warn', 'message1', 'message2', {
375+
key: 'value',
376+
})
377+
expect(consoleWarnSpy).toHaveBeenCalledTimes(1)
378+
expect(consoleWarnSpy).toHaveBeenCalledWith(
379+
'[@launchdarkly plugins]: (test-context-5): ',
380+
'message1',
381+
'message2',
382+
{ key: 'value' },
383+
)
384+
385+
// Second call should not log
386+
internalLogOnce(context, logOnceId, 'warn', 'different', 'args')
387+
expect(consoleWarnSpy).toHaveBeenCalledTimes(1) // Still only called once
388+
})
389+
})

sdk/highlight-run/src/client/utils/storage.ts

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Cookies from 'js-cookie'
22
import { SESSION_PUSH_THRESHOLD } from '../constants/sessions'
3-
import { internalLog } from '../../sdk/util'
3+
import { internalLogOnce } from '../../sdk/util'
44

55
export enum LocalStorageKeys {
66
CLIENT_ID = 'highlightClientID',
@@ -50,16 +50,41 @@ export const globalStorage = new Storage()
5050
export const cookieStorage = new CookieStorage()
5151

5252
const getPersistentStorage = () => {
53+
let storage:
54+
| Storage
55+
| typeof window.localStorage
56+
| typeof window.sessionStorage
57+
| null = null
58+
5359
try {
5460
switch (mode) {
5561
case 'localStorage':
56-
return window.localStorage
62+
storage = window.localStorage
63+
break
5764
case 'sessionStorage':
58-
return window.sessionStorage
65+
storage = window.sessionStorage
66+
break
67+
}
68+
if (!storage) {
69+
internalLogOnce(
70+
'storage',
71+
'getPersistentStorage',
72+
'debug',
73+
`persistent storage was not found for mode ${mode}; using global storage`,
74+
)
75+
storage = globalStorage
5976
}
6077
} catch (e) {
61-
return globalStorage
78+
internalLogOnce(
79+
'storage',
80+
'getPersistentStorage',
81+
'debug',
82+
`failed to get persistent storage; using global storage`,
83+
e,
84+
)
85+
storage = globalStorage
6286
}
87+
return storage
6388
}
6489

6590
export const setStorageMode = (m: Mode) => {
@@ -74,8 +99,9 @@ export const getItem = (key: string) => {
7499
try {
75100
return getPersistentStorage().getItem(key)
76101
} catch (e) {
77-
internalLog(
102+
internalLogOnce(
78103
`storage`,
104+
'getItem',
79105
'debug',
80106
`failed to get item ${key}; using global storage`,
81107
e,
@@ -89,8 +115,9 @@ export const setItem = (key: string, value: string) => {
89115
try {
90116
return getPersistentStorage().setItem(key, value)
91117
} catch (e) {
92-
internalLog(
118+
internalLogOnce(
93119
`storage`,
120+
'setItem',
94121
'debug',
95122
`failed to set item ${key}; using global storage`,
96123
e,
@@ -104,8 +131,9 @@ export const removeItem = (key: string) => {
104131
try {
105132
return getPersistentStorage().removeItem(key)
106133
} catch (e) {
107-
internalLog(
134+
internalLogOnce(
108135
`storage`,
136+
'removeItem',
109137
'debug',
110138
`failed to remove item ${key}; using global storage`,
111139
e,

sdk/highlight-run/src/sdk/util.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// Object for tracking previously logged messages.
2+
const loggedIds = Object.create(null)
3+
14
export const internalLog = (
25
context: string,
36
level: keyof Console,
@@ -8,6 +11,31 @@ export const internalLog = (
811
void reportLog(prefix, ...msg)
912
}
1013

14+
/**
15+
* Utility to help avoid logging the same message multiple times.
16+
*
17+
* This is helpful to prevent spamming the logs with sticky error conditions.
18+
* For example if local storage cannot be written to, then each attempt to
19+
* write would fail, and logging each time would spam the logs.
20+
*
21+
* @param logOnceId - A message with this ID will only be logged one time for the given context.
22+
* @param context - The context of the message.
23+
* @param level - The level of the message.
24+
* @param msg - The message to log.
25+
*/
26+
export const internalLogOnce = (
27+
context: string,
28+
logOnceId: string,
29+
level: keyof Console,
30+
...msg: any
31+
) => {
32+
if (loggedIds[`${context}-${logOnceId}`]) {
33+
return
34+
}
35+
loggedIds[`${context}-${logOnceId}`] = true
36+
internalLog(context, level, ...msg)
37+
}
38+
1139
const reportLog = async (prefix: string, ...msg: any) => {
1240
try {
1341
const { LDObserve } = await import('./LDObserve')

0 commit comments

Comments
 (0)