@envive-ai/react-hooks 0.3.56 → 0.3.57
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/dist/application/commerce-api.cjs +11 -12
- package/dist/application/commerce-api.js +11 -12
- package/dist/application/logging/index.cjs +13 -0
- package/dist/application/logging/index.d.cts +3 -0
- package/dist/application/logging/index.d.ts +3 -0
- package/dist/application/logging/index.js +4 -0
- package/dist/application/logging/logLevel.cjs +123 -0
- package/dist/application/logging/logLevel.d.cts +47 -0
- package/dist/application/logging/logLevel.d.ts +47 -0
- package/dist/application/logging/logLevel.js +115 -0
- package/dist/application/logging/logger.cjs +6 -1
- package/dist/application/logging/logger.d.cts +13 -0
- package/dist/application/logging/logger.d.ts +13 -0
- package/dist/application/logging/logger.js +7 -1
- package/dist/atoms/app/index.d.cts +7 -7
- package/dist/atoms/app/index.d.ts +7 -7
- package/dist/atoms/app/variant.d.cts +6 -6
- package/dist/atoms/app/variant.d.ts +6 -6
- package/dist/atoms/chat/chatState.d.cts +19 -19
- package/dist/atoms/chat/chatState.d.ts +1 -1
- package/dist/atoms/chat/form.d.cts +2 -2
- package/dist/atoms/chat/form.d.ts +2 -2
- package/dist/atoms/chat/index.d.cts +2 -2
- package/dist/atoms/chat/lastMessage.d.cts +2 -2
- package/dist/atoms/chat/lastMessage.d.ts +2 -2
- package/dist/atoms/chat/messageQueue.d.cts +6 -6
- package/dist/atoms/chat/messageQueue.d.ts +6 -6
- package/dist/atoms/chat/performanceMetrics.d.cts +6 -6
- package/dist/atoms/chat/performanceMetrics.d.ts +6 -6
- package/dist/atoms/chat/renderedWidgetRefs.d.cts +2 -2
- package/dist/atoms/chat/renderedWidgetRefs.d.ts +2 -2
- package/dist/atoms/chat/replies.d.cts +3 -3
- package/dist/atoms/chat/suggestions.d.cts +2 -2
- package/dist/atoms/chat/suggestions.d.ts +2 -2
- package/dist/atoms/envive/enviveConfig.d.cts +14 -14
- package/dist/atoms/envive/enviveConfig.d.ts +14 -14
- package/dist/atoms/globalSearch/globalSearch.d.cts +5 -5
- package/dist/atoms/globalSearch/globalSearch.d.ts +5 -5
- package/dist/atoms/org/customerService.d.cts +6 -6
- package/dist/atoms/org/customerService.d.ts +6 -6
- package/dist/atoms/org/graphqlConfig.d.cts +4 -4
- package/dist/atoms/org/graphqlConfig.d.ts +5 -5
- package/dist/atoms/org/newOrgConfigAtom.d.cts +2 -2
- package/dist/atoms/org/newOrgConfigAtom.d.ts +3 -3
- package/dist/atoms/org/orgAnalyticsConfig.d.ts +5 -5
- package/dist/atoms/search/chatSearch.d.cts +17 -17
- package/dist/atoms/search/chatSearch.d.ts +17 -17
- package/dist/atoms/search/searchAPI.d.cts +13 -13
- package/dist/atoms/search/types.d.ts +1 -1
- package/dist/atoms/widget/chatPreviewLoading.d.cts +2 -2
- package/dist/atoms/widget/chatPreviewLoading.d.ts +2 -2
- package/dist/contexts/salesAgentContext/salesAgentService.cjs +2 -2
- package/dist/contexts/salesAgentContext/salesAgentService.js +2 -2
- package/dist/contexts/systemSettingsContext/systemSettingsContext.d.ts +2 -2
- package/dist/contexts/types.d.cts +1 -1
- package/dist/contexts/types.d.ts +1 -1
- package/dist/contexts/typesV3.d.cts +1 -1
- package/dist/contexts/typesV3.d.ts +1 -1
- package/dist/hooks/ChatToggle/useChatToggle.cjs +10 -8
- package/dist/hooks/ChatToggle/useChatToggle.d.cts +4 -3
- package/dist/hooks/ChatToggle/useChatToggle.d.ts +4 -3
- package/dist/hooks/ChatToggle/useChatToggle.js +11 -9
- package/dist/hooks/GrabAndScroll/useGrabAndScroll.d.ts +2 -2
- package/dist/hooks/Intersection/useIntersection.cjs +7 -5
- package/dist/hooks/Intersection/useIntersection.js +7 -5
- package/dist/hooks/Search/useSearchInput.cjs +1 -1
- package/dist/hooks/Search/useSearchInput.js +1 -1
- package/dist/hooks/ShopifyUrlOperations/useShopifyUrlOperations.d.cts +2 -2
- package/dist/hooks/ShopifyUrlOperations/useShopifyUrlOperations.d.ts +2 -2
- package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.cjs +8 -6
- package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.js +8 -6
- package/dist/hooks/WidgetInteraction/types.cjs +40 -1
- package/dist/hooks/WidgetInteraction/types.d.cts +16 -2
- package/dist/hooks/WidgetInteraction/types.d.ts +16 -2
- package/dist/hooks/WidgetInteraction/types.js +41 -2
- package/dist/hooks/utils.d.cts +1 -1
- package/dist/hooks/utils.d.ts +1 -1
- package/dist/services/amplitudeService/amplitudeService.cjs +14 -5
- package/dist/services/amplitudeService/amplitudeService.js +14 -5
- package/package.json +5 -1
- package/src/application/__tests__/commerceApiStreaming.test.ts +171 -0
- package/src/application/commerce-api.ts +21 -21
- package/src/application/logging/__tests__/logger.test.ts +159 -0
- package/src/application/logging/index.ts +12 -0
- package/src/application/logging/logLevel.ts +162 -0
- package/src/application/logging/logger.ts +13 -0
- package/src/contexts/salesAgentContext/salesAgentService.ts +1 -1
- package/src/contexts/userIdentityContext/__tests__/userIdentityContext.test.tsx +4 -5
- package/src/hooks/ChatToggle/__tests__/useChatToggle.test.ts +273 -0
- package/src/hooks/ChatToggle/useChatToggle.ts +39 -12
- package/src/hooks/Intersection/useIntersection.ts +7 -4
- package/src/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.ts +8 -5
- package/src/hooks/WidgetInteraction/__tests__/chatTriggerWidget.test.ts +51 -0
- package/src/hooks/WidgetInteraction/types.ts +53 -0
- package/src/services/amplitudeService/__tests__/amplitudeService.test.ts +76 -0
- package/src/services/amplitudeService/amplitudeService.ts +37 -5
|
@@ -263,34 +263,24 @@ class CommerceApiClient {
|
|
|
263
263
|
const reader = response.raw.body.getReader();
|
|
264
264
|
const decoder = new TextDecoder('utf-8');
|
|
265
265
|
|
|
266
|
+
// Buffer holding the trailing incomplete line of the previous chunk
|
|
266
267
|
let partial = '';
|
|
267
|
-
|
|
268
|
+
|
|
268
269
|
const safeParse = (line: string): unknown => {
|
|
269
270
|
try {
|
|
270
271
|
return JSON.parse(line); // Parse the JSON string
|
|
271
272
|
} catch (err) {
|
|
272
|
-
logger.logError('Error parsing streamed line', err, {
|
|
273
|
-
|
|
274
|
-
partial,
|
|
275
|
-
});
|
|
276
|
-
// swallow the error and set the partial to our current chunk
|
|
277
|
-
partial = line;
|
|
278
|
-
return partial;
|
|
273
|
+
logger.logError('Error parsing streamed line', err, { line });
|
|
274
|
+
return null;
|
|
279
275
|
}
|
|
280
276
|
};
|
|
281
277
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
// merge the partial with the incoming chunk
|
|
285
|
-
const lines = `${partial}${chunk}`.split('\n');
|
|
286
|
-
|
|
287
|
-
const parsedLines = lines
|
|
278
|
+
const parseLines = (lines: string[]): unknown[] =>
|
|
279
|
+
lines
|
|
288
280
|
.map(line => line.replace(/^data: /, '').trim()) // Remove the "data: " prefix
|
|
289
281
|
.filter(line => line !== '' && line !== '[DONE]') // Remove empty lines and "[DONE]"
|
|
290
282
|
.map(safeParse)
|
|
291
|
-
.filter(v => v);
|
|
292
|
-
return parsedLines;
|
|
293
|
-
};
|
|
283
|
+
.filter(v => v != null);
|
|
294
284
|
|
|
295
285
|
while (true) {
|
|
296
286
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -300,11 +290,12 @@ class CommerceApiClient {
|
|
|
300
290
|
break;
|
|
301
291
|
}
|
|
302
292
|
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
-
const
|
|
293
|
+
// A chunk boundary can fall mid-line, so only parse the lines that
|
|
294
|
+
// are terminated by a newline and hold the rest until the next chunk
|
|
295
|
+
const lines = `${partial}${decoder.decode(value, { stream: true })}`.split('\n');
|
|
296
|
+
partial = lines.pop() ?? '';
|
|
306
297
|
|
|
307
|
-
for (const parsedLine of
|
|
298
|
+
for (const parsedLine of parseLines(lines)) {
|
|
308
299
|
const validatedResponse = validateResponse(parsedLine);
|
|
309
300
|
|
|
310
301
|
if (validatedResponse) {
|
|
@@ -312,6 +303,15 @@ class CommerceApiClient {
|
|
|
312
303
|
}
|
|
313
304
|
}
|
|
314
305
|
}
|
|
306
|
+
|
|
307
|
+
// Flush the remainder of a stream that didn't end with a newline
|
|
308
|
+
for (const parsedLine of parseLines([`${partial}${decoder.decode()}`])) {
|
|
309
|
+
const validatedResponse = validateResponse(parsedLine);
|
|
310
|
+
|
|
311
|
+
if (validatedResponse) {
|
|
312
|
+
yield validatedResponse;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
315
|
} catch (error: unknown) {
|
|
316
316
|
logger.logError('Failed to get next streaming responses', error, {
|
|
317
317
|
payloadContext: payload?.context,
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import Logger from 'src/application/logging/logger';
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_LOG_LEVEL,
|
|
4
|
+
LOG_LEVEL_STORAGE_KEY,
|
|
5
|
+
getLogLevel,
|
|
6
|
+
isLevelEnabled,
|
|
7
|
+
resetLogLevelForTesting,
|
|
8
|
+
setLogLevel,
|
|
9
|
+
} from 'src/application/logging/logLevel';
|
|
10
|
+
|
|
11
|
+
const setHref = (href: string) => {
|
|
12
|
+
Object.defineProperty(window, 'location', {
|
|
13
|
+
configurable: true,
|
|
14
|
+
value: { ...window.location, href, search: new URL(href).search },
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe('logger log level', () => {
|
|
19
|
+
const originalHref = window.location.href;
|
|
20
|
+
let spies: ReturnType<typeof vi.spyOn>[] = [];
|
|
21
|
+
let logger: Logger;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
resetLogLevelForTesting();
|
|
25
|
+
window.localStorage.clear();
|
|
26
|
+
setHref('https://merchant.example.com/products/thing');
|
|
27
|
+
delete (window as { _envive?: unknown })._envive;
|
|
28
|
+
delete (window as { _spiffy?: unknown })._spiffy;
|
|
29
|
+
logger = new Logger('testCaller');
|
|
30
|
+
spies = [
|
|
31
|
+
vi.spyOn(console, 'debug').mockImplementation(() => {}),
|
|
32
|
+
vi.spyOn(console, 'info').mockImplementation(() => {}),
|
|
33
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {}),
|
|
34
|
+
vi.spyOn(console, 'error').mockImplementation(() => {}),
|
|
35
|
+
];
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
spies.forEach(spy => spy.mockRestore());
|
|
40
|
+
resetLogLevelForTesting();
|
|
41
|
+
setHref(originalHref);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const emitAll = () => {
|
|
45
|
+
logger.logDebug('d');
|
|
46
|
+
logger.logInfo('i');
|
|
47
|
+
logger.logWarn('w', undefined);
|
|
48
|
+
logger.logError('e', undefined);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const emitted = () => ({
|
|
52
|
+
debug: (console.debug as unknown as { mock: { calls: unknown[] } }).mock.calls.length,
|
|
53
|
+
info: (console.info as unknown as { mock: { calls: unknown[] } }).mock.calls.length,
|
|
54
|
+
warn: (console.warn as unknown as { mock: { calls: unknown[] } }).mock.calls.length,
|
|
55
|
+
error: (console.error as unknown as { mock: { calls: unknown[] } }).mock.calls.length,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('mutes debug and info but keeps warn and error by default', () => {
|
|
59
|
+
expect(DEFAULT_LOG_LEVEL).toBe('warn');
|
|
60
|
+
expect(getLogLevel()).toBe('warn');
|
|
61
|
+
|
|
62
|
+
emitAll();
|
|
63
|
+
|
|
64
|
+
expect(emitted()).toEqual({ debug: 0, info: 0, warn: 1, error: 1 });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('silences every level at "silent"', () => {
|
|
68
|
+
setLogLevel('silent');
|
|
69
|
+
|
|
70
|
+
emitAll();
|
|
71
|
+
|
|
72
|
+
expect(emitted()).toEqual({ debug: 0, info: 0, warn: 0, error: 0 });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('emits every level at "debug"', () => {
|
|
76
|
+
setLogLevel('debug');
|
|
77
|
+
|
|
78
|
+
emitAll();
|
|
79
|
+
|
|
80
|
+
expect(emitted()).toEqual({ debug: 1, info: 1, warn: 1, error: 1 });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('keeps the existing output format when a level is enabled', () => {
|
|
84
|
+
setLogLevel('debug');
|
|
85
|
+
|
|
86
|
+
logger.logDebug('a message', { extra: true });
|
|
87
|
+
|
|
88
|
+
expect(console.debug).toHaveBeenCalledWith(
|
|
89
|
+
expect.stringMatching(/^DEBUG: \[envive-ai\] .+ - testCaller - a message$/),
|
|
90
|
+
{ extra: true },
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('honours the window override without a reload', () => {
|
|
95
|
+
expect(isLevelEnabled('debug')).toBe(false);
|
|
96
|
+
|
|
97
|
+
(window as { _envive?: { logLevel?: string } })._envive = { logLevel: 'debug' };
|
|
98
|
+
|
|
99
|
+
expect(isLevelEnabled('debug')).toBe(true);
|
|
100
|
+
expect(getLogLevel()).toBe('debug');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('accepts the _spiffy window override and is case-insensitive', () => {
|
|
104
|
+
(window as { _spiffy?: { logLevel?: string } })._spiffy = { logLevel: 'DEBUG' };
|
|
105
|
+
|
|
106
|
+
expect(getLogLevel()).toBe('debug');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('ignores an unrecognised level and falls back to the default', () => {
|
|
110
|
+
(window as { _envive?: { logLevel?: string } })._envive = { logLevel: 'chatty' };
|
|
111
|
+
|
|
112
|
+
expect(getLogLevel()).toBe(DEFAULT_LOG_LEVEL);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('reads the level from the query param and persists it for later navigations', () => {
|
|
116
|
+
setHref('https://merchant.example.com/products/thing?envive_log_level=debug');
|
|
117
|
+
|
|
118
|
+
expect(getLogLevel()).toBe('debug');
|
|
119
|
+
expect(window.localStorage.getItem(LOG_LEVEL_STORAGE_KEY)).toBe('debug');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('applies a persisted level on a page without the query param', () => {
|
|
123
|
+
window.localStorage.setItem(LOG_LEVEL_STORAGE_KEY, 'info');
|
|
124
|
+
|
|
125
|
+
expect(getLogLevel()).toBe('info');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('lets the query param win over a level pinned programmatically', () => {
|
|
129
|
+
setHref('https://merchant.example.com/products/thing?envive_log_level=debug');
|
|
130
|
+
setLogLevel('silent');
|
|
131
|
+
|
|
132
|
+
expect(getLogLevel()).toBe('debug');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('lets setLogLevel(undefined) fall back to the levers below it', () => {
|
|
136
|
+
window.localStorage.setItem(LOG_LEVEL_STORAGE_KEY, 'info');
|
|
137
|
+
setLogLevel('silent');
|
|
138
|
+
expect(getLogLevel()).toBe('silent');
|
|
139
|
+
|
|
140
|
+
setLogLevel(undefined);
|
|
141
|
+
|
|
142
|
+
expect(getLogLevel()).toBe('info');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('survives localStorage throwing', () => {
|
|
146
|
+
const getItem = vi.spyOn(window.localStorage, 'getItem').mockImplementation(() => {
|
|
147
|
+
throw new Error('blocked');
|
|
148
|
+
});
|
|
149
|
+
const setItem = vi.spyOn(window.localStorage, 'setItem').mockImplementation(() => {
|
|
150
|
+
throw new Error('blocked');
|
|
151
|
+
});
|
|
152
|
+
setHref('https://merchant.example.com/products/thing?envive_log_level=debug');
|
|
153
|
+
|
|
154
|
+
expect(getLogLevel()).toBe('debug');
|
|
155
|
+
|
|
156
|
+
getItem.mockRestore();
|
|
157
|
+
setItem.mockRestore();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { default } from 'src/application/logging/logger';
|
|
2
|
+
export {
|
|
3
|
+
DEFAULT_LOG_LEVEL,
|
|
4
|
+
getLogLevel,
|
|
5
|
+
isLevelEnabled,
|
|
6
|
+
LOG_LEVEL_QUERY_PARAM,
|
|
7
|
+
LOG_LEVEL_STORAGE_KEY,
|
|
8
|
+
LOG_LEVELS,
|
|
9
|
+
resetLogLevelForTesting,
|
|
10
|
+
setLogLevel,
|
|
11
|
+
} from 'src/application/logging/logLevel';
|
|
12
|
+
export type { LogLevel } from 'src/application/logging/logLevel';
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console log-level gate shared by every package's Logger.
|
|
3
|
+
*
|
|
4
|
+
* Debug and info are muted by default: a single merchant page load produces
|
|
5
|
+
* ~950 debug lines (the bulk of them from featureFlagService checking gates on
|
|
6
|
+
* every render), which is unusable noise on a customer's site.
|
|
7
|
+
*
|
|
8
|
+
* To turn logging back on, in priority order:
|
|
9
|
+
*
|
|
10
|
+
* 1. Query param — `?envive_log_level=debug`
|
|
11
|
+
* The support lever. Wins over everything else so logging can always be
|
|
12
|
+
* re-enabled on a live merchant page, even if the embedding page pinned a
|
|
13
|
+
* level at bootstrap. The chosen level is persisted to localStorage so it
|
|
14
|
+
* survives navigation (a support session spans more than one page view);
|
|
15
|
+
* clear it with `?envive_log_level=warn` or by removing the stored key.
|
|
16
|
+
*
|
|
17
|
+
* 2. `setLogLevel('debug')` — programmatic, e.g. bootstrap code or tests.
|
|
18
|
+
*
|
|
19
|
+
* 3. `window._envive.logLevel = 'debug'` (or `window._spiffy.logLevel`)
|
|
20
|
+
* Read on every call, so it can be flipped from the browser console
|
|
21
|
+
* mid-session without a reload.
|
|
22
|
+
*
|
|
23
|
+
* 4. `localStorage['envive-log-level']` — sticky per browser.
|
|
24
|
+
*
|
|
25
|
+
* 5. Default: 'warn'.
|
|
26
|
+
*
|
|
27
|
+
* Levels, least to most verbose: silent < error < warn < info < debug.
|
|
28
|
+
* A message emits when its own level is at or below the resolved level, so
|
|
29
|
+
* 'silent' mutes everything including errors.
|
|
30
|
+
*
|
|
31
|
+
* Kept free of React imports (like featureFlagService): the injection bundle
|
|
32
|
+
* logs from its pre-config code path, before it decides whether to load the
|
|
33
|
+
* React app, and pulling React in here would put it on the eager chunk.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
export const LOG_LEVELS = ['silent', 'error', 'warn', 'info', 'debug'] as const;
|
|
37
|
+
|
|
38
|
+
export type LogLevel = (typeof LOG_LEVELS)[number];
|
|
39
|
+
|
|
40
|
+
const LEVEL_SEVERITY: Record<LogLevel, number> = {
|
|
41
|
+
silent: 0,
|
|
42
|
+
error: 1,
|
|
43
|
+
warn: 2,
|
|
44
|
+
info: 3,
|
|
45
|
+
debug: 4,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/* Errors and warnings only. logDebug/logInfo make up the bulk of the call sites
|
|
49
|
+
* in this repo and used to print on every merchant page load. */
|
|
50
|
+
export const DEFAULT_LOG_LEVEL: LogLevel = 'warn';
|
|
51
|
+
|
|
52
|
+
export const LOG_LEVEL_QUERY_PARAM = 'envive_log_level';
|
|
53
|
+
export const LOG_LEVEL_STORAGE_KEY = 'envive-log-level';
|
|
54
|
+
|
|
55
|
+
const asLogLevel = (value: unknown): LogLevel | undefined => {
|
|
56
|
+
if (typeof value !== 'string') {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const normalized = value.toLowerCase();
|
|
60
|
+
return (LOG_LEVELS as readonly string[]).includes(normalized)
|
|
61
|
+
? (normalized as LogLevel)
|
|
62
|
+
: undefined;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/* Set by setLogLevel(). Overrides window/stored/default, but deliberately not
|
|
66
|
+
* the query param, so support can always turn logging back on in a live session
|
|
67
|
+
* even if the embedding page pinned a level at bootstrap. */
|
|
68
|
+
let programmaticLevel: LogLevel | undefined;
|
|
69
|
+
|
|
70
|
+
/* The query param and localStorage cannot change without a navigation, so both
|
|
71
|
+
* are read once. `undefined` means "not resolved yet" and `null` caches a miss,
|
|
72
|
+
* keeping repeat lookups off the logging hot path. */
|
|
73
|
+
let cachedQueryLevel: LogLevel | null | undefined;
|
|
74
|
+
let cachedStoredLevel: LogLevel | null | undefined;
|
|
75
|
+
|
|
76
|
+
/* Persists the level so it survives navigation — a support session spans more
|
|
77
|
+
* than one page view. */
|
|
78
|
+
const storeLevel = (level: LogLevel): void => {
|
|
79
|
+
if (typeof window === 'undefined' || typeof window.localStorage === 'undefined') {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
window.localStorage.setItem(LOG_LEVEL_STORAGE_KEY, level);
|
|
84
|
+
} catch {
|
|
85
|
+
// Storage can be full or blocked; the level still applies to this page.
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const readQueryLevel = (): LogLevel | undefined => {
|
|
90
|
+
if (typeof window === 'undefined') {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
const url = window.location?.href ?? '';
|
|
94
|
+
if (!url.includes('?')) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const params = new URLSearchParams(new URL(url).search);
|
|
99
|
+
return asLogLevel(params.get(LOG_LEVEL_QUERY_PARAM));
|
|
100
|
+
} catch {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const queryLevel = (): LogLevel | undefined => {
|
|
106
|
+
if (cachedQueryLevel === undefined) {
|
|
107
|
+
const level = readQueryLevel();
|
|
108
|
+
if (level) {
|
|
109
|
+
storeLevel(level);
|
|
110
|
+
}
|
|
111
|
+
cachedQueryLevel = level ?? null;
|
|
112
|
+
}
|
|
113
|
+
return cachedQueryLevel ?? undefined;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const storedLevel = (): LogLevel | undefined => {
|
|
117
|
+
if (cachedStoredLevel === undefined) {
|
|
118
|
+
let level: LogLevel | undefined;
|
|
119
|
+
if (typeof window !== 'undefined' && typeof window.localStorage !== 'undefined') {
|
|
120
|
+
try {
|
|
121
|
+
level = asLogLevel(window.localStorage.getItem(LOG_LEVEL_STORAGE_KEY));
|
|
122
|
+
} catch {
|
|
123
|
+
level = undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
cachedStoredLevel = level ?? null;
|
|
127
|
+
}
|
|
128
|
+
return cachedStoredLevel ?? undefined;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/* Read on every call, so it can be flipped from the browser console mid-session
|
|
132
|
+
* without a reload. */
|
|
133
|
+
const windowLevel = (): LogLevel | undefined => {
|
|
134
|
+
if (typeof window === 'undefined') {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
const windowOverride = window as Window & {
|
|
138
|
+
_envive?: { logLevel?: string };
|
|
139
|
+
_spiffy?: { logLevel?: string };
|
|
140
|
+
};
|
|
141
|
+
return asLogLevel(windowOverride._envive?.logLevel ?? windowOverride._spiffy?.logLevel);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/* Resolution order mirrors featureFlagService's override chain: query param >
|
|
145
|
+
* setLogLevel() > window._envive.logLevel > localStorage > default. */
|
|
146
|
+
export const getLogLevel = (): LogLevel =>
|
|
147
|
+
queryLevel() ?? programmaticLevel ?? windowLevel() ?? storedLevel() ?? DEFAULT_LOG_LEVEL;
|
|
148
|
+
|
|
149
|
+
/* Pass undefined to clear the override and fall back to the levers above. */
|
|
150
|
+
export const setLogLevel = (level: LogLevel | undefined): void => {
|
|
151
|
+
programmaticLevel = level;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export const isLevelEnabled = (level: Exclude<LogLevel, 'silent'>): boolean =>
|
|
155
|
+
LEVEL_SEVERITY[getLogLevel()] >= LEVEL_SEVERITY[level];
|
|
156
|
+
|
|
157
|
+
/* Test seam: drops the memoized query-param and localStorage resolution. */
|
|
158
|
+
export const resetLogLevelForTesting = (): void => {
|
|
159
|
+
programmaticLevel = undefined;
|
|
160
|
+
cachedQueryLevel = undefined;
|
|
161
|
+
cachedStoredLevel = undefined;
|
|
162
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
|
+
import { isLevelEnabled } from 'src/application/logging/logLevel';
|
|
2
3
|
|
|
3
4
|
class Logger {
|
|
4
5
|
private readonly caller: string;
|
|
@@ -15,6 +16,9 @@ class Logger {
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
logInfo(message: string, ...args: unknown[]): void {
|
|
19
|
+
if (!isLevelEnabled('info')) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
18
22
|
console.info(
|
|
19
23
|
`INFO: [envive-ai] ${Logger.getTimestamp()} - ${this.caller} - ${message}`,
|
|
20
24
|
...args,
|
|
@@ -22,6 +26,9 @@ class Logger {
|
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
logDebug(message: string, ...args: unknown[]): void {
|
|
29
|
+
if (!isLevelEnabled('debug')) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
25
32
|
console.debug(
|
|
26
33
|
`DEBUG: [envive-ai] ${Logger.getTimestamp()} - ${this.caller} - ${message}`,
|
|
27
34
|
...args,
|
|
@@ -29,6 +36,9 @@ class Logger {
|
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
logError(message: string, error: unknown | undefined, ...args: unknown[]): void {
|
|
39
|
+
if (!isLevelEnabled('error')) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
32
42
|
console.error(
|
|
33
43
|
`ERROR: [envive-ai] ${Logger.getTimestamp()} - ${this.caller} - ${message} error=${error}`,
|
|
34
44
|
args,
|
|
@@ -36,6 +46,9 @@ class Logger {
|
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
logWarn(message: string, error: unknown | undefined, ...args: unknown[]): void {
|
|
49
|
+
if (!isLevelEnabled('warn')) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
39
52
|
console.warn(
|
|
40
53
|
`WARN: [envive-ai] ${Logger.getTimestamp()} - ${this.caller} - ${message} error=${error}`,
|
|
41
54
|
args,
|
|
@@ -313,7 +313,7 @@ export const useSalesAgentService: () => SalesAgentService = () => {
|
|
|
313
313
|
// },
|
|
314
314
|
// });
|
|
315
315
|
} catch (e) {
|
|
316
|
-
|
|
316
|
+
logger.logError('error getting streaming responses', e);
|
|
317
317
|
successfulResponse = false;
|
|
318
318
|
// Log failed next_responses call
|
|
319
319
|
// const responseTime = Date.now() - startTime;
|
|
@@ -177,13 +177,12 @@ describe('UserIdentityProvider', () => {
|
|
|
177
177
|
it('should allow components to access context via useUserIdentity hook', async () => {
|
|
178
178
|
renderWithProviders(<MockComponent />);
|
|
179
179
|
|
|
180
|
+
// Verify all context methods are accessible. The user id is populated by an async
|
|
181
|
+
// effect, so the content assertions must wait for it rather than assert right away.
|
|
180
182
|
await waitFor(() => {
|
|
181
|
-
expect(screen.getByTestId('user-id')).
|
|
183
|
+
expect(screen.getByTestId('user-id').textContent).toContain('spiffy-user-id-');
|
|
184
|
+
expect(screen.getByTestId('is-ready').textContent).toBe('true');
|
|
182
185
|
});
|
|
183
|
-
|
|
184
|
-
// Verify all context methods are accessible
|
|
185
|
-
expect(screen.getByTestId('user-id').textContent).toContain('spiffy-user-id-');
|
|
186
|
-
expect(screen.getByTestId('is-ready').textContent).toBe('true');
|
|
187
186
|
});
|
|
188
187
|
|
|
189
188
|
it('should throw error when used outside of UserIdentityProvider', () => {
|