@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.
Files changed (96) hide show
  1. package/dist/application/commerce-api.cjs +11 -12
  2. package/dist/application/commerce-api.js +11 -12
  3. package/dist/application/logging/index.cjs +13 -0
  4. package/dist/application/logging/index.d.cts +3 -0
  5. package/dist/application/logging/index.d.ts +3 -0
  6. package/dist/application/logging/index.js +4 -0
  7. package/dist/application/logging/logLevel.cjs +123 -0
  8. package/dist/application/logging/logLevel.d.cts +47 -0
  9. package/dist/application/logging/logLevel.d.ts +47 -0
  10. package/dist/application/logging/logLevel.js +115 -0
  11. package/dist/application/logging/logger.cjs +6 -1
  12. package/dist/application/logging/logger.d.cts +13 -0
  13. package/dist/application/logging/logger.d.ts +13 -0
  14. package/dist/application/logging/logger.js +7 -1
  15. package/dist/atoms/app/index.d.cts +7 -7
  16. package/dist/atoms/app/index.d.ts +7 -7
  17. package/dist/atoms/app/variant.d.cts +6 -6
  18. package/dist/atoms/app/variant.d.ts +6 -6
  19. package/dist/atoms/chat/chatState.d.cts +19 -19
  20. package/dist/atoms/chat/chatState.d.ts +1 -1
  21. package/dist/atoms/chat/form.d.cts +2 -2
  22. package/dist/atoms/chat/form.d.ts +2 -2
  23. package/dist/atoms/chat/index.d.cts +2 -2
  24. package/dist/atoms/chat/lastMessage.d.cts +2 -2
  25. package/dist/atoms/chat/lastMessage.d.ts +2 -2
  26. package/dist/atoms/chat/messageQueue.d.cts +6 -6
  27. package/dist/atoms/chat/messageQueue.d.ts +6 -6
  28. package/dist/atoms/chat/performanceMetrics.d.cts +6 -6
  29. package/dist/atoms/chat/performanceMetrics.d.ts +6 -6
  30. package/dist/atoms/chat/renderedWidgetRefs.d.cts +2 -2
  31. package/dist/atoms/chat/renderedWidgetRefs.d.ts +2 -2
  32. package/dist/atoms/chat/replies.d.cts +3 -3
  33. package/dist/atoms/chat/suggestions.d.cts +2 -2
  34. package/dist/atoms/chat/suggestions.d.ts +2 -2
  35. package/dist/atoms/envive/enviveConfig.d.cts +14 -14
  36. package/dist/atoms/envive/enviveConfig.d.ts +14 -14
  37. package/dist/atoms/globalSearch/globalSearch.d.cts +5 -5
  38. package/dist/atoms/globalSearch/globalSearch.d.ts +5 -5
  39. package/dist/atoms/org/customerService.d.cts +6 -6
  40. package/dist/atoms/org/customerService.d.ts +6 -6
  41. package/dist/atoms/org/graphqlConfig.d.cts +4 -4
  42. package/dist/atoms/org/graphqlConfig.d.ts +5 -5
  43. package/dist/atoms/org/newOrgConfigAtom.d.cts +2 -2
  44. package/dist/atoms/org/newOrgConfigAtom.d.ts +3 -3
  45. package/dist/atoms/org/orgAnalyticsConfig.d.ts +5 -5
  46. package/dist/atoms/search/chatSearch.d.cts +17 -17
  47. package/dist/atoms/search/chatSearch.d.ts +17 -17
  48. package/dist/atoms/search/searchAPI.d.cts +13 -13
  49. package/dist/atoms/search/types.d.ts +1 -1
  50. package/dist/atoms/widget/chatPreviewLoading.d.cts +2 -2
  51. package/dist/atoms/widget/chatPreviewLoading.d.ts +2 -2
  52. package/dist/contexts/salesAgentContext/salesAgentService.cjs +2 -2
  53. package/dist/contexts/salesAgentContext/salesAgentService.js +2 -2
  54. package/dist/contexts/systemSettingsContext/systemSettingsContext.d.ts +2 -2
  55. package/dist/contexts/types.d.cts +1 -1
  56. package/dist/contexts/types.d.ts +1 -1
  57. package/dist/contexts/typesV3.d.cts +1 -1
  58. package/dist/contexts/typesV3.d.ts +1 -1
  59. package/dist/hooks/ChatToggle/useChatToggle.cjs +10 -8
  60. package/dist/hooks/ChatToggle/useChatToggle.d.cts +4 -3
  61. package/dist/hooks/ChatToggle/useChatToggle.d.ts +4 -3
  62. package/dist/hooks/ChatToggle/useChatToggle.js +11 -9
  63. package/dist/hooks/GrabAndScroll/useGrabAndScroll.d.ts +2 -2
  64. package/dist/hooks/Intersection/useIntersection.cjs +7 -5
  65. package/dist/hooks/Intersection/useIntersection.js +7 -5
  66. package/dist/hooks/Search/useSearchInput.cjs +1 -1
  67. package/dist/hooks/Search/useSearchInput.js +1 -1
  68. package/dist/hooks/ShopifyUrlOperations/useShopifyUrlOperations.d.cts +2 -2
  69. package/dist/hooks/ShopifyUrlOperations/useShopifyUrlOperations.d.ts +2 -2
  70. package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.cjs +8 -6
  71. package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.js +8 -6
  72. package/dist/hooks/WidgetInteraction/types.cjs +40 -1
  73. package/dist/hooks/WidgetInteraction/types.d.cts +16 -2
  74. package/dist/hooks/WidgetInteraction/types.d.ts +16 -2
  75. package/dist/hooks/WidgetInteraction/types.js +41 -2
  76. package/dist/hooks/utils.d.cts +1 -1
  77. package/dist/hooks/utils.d.ts +1 -1
  78. package/dist/services/amplitudeService/amplitudeService.cjs +14 -5
  79. package/dist/services/amplitudeService/amplitudeService.js +14 -5
  80. package/package.json +5 -1
  81. package/src/application/__tests__/commerceApiStreaming.test.ts +171 -0
  82. package/src/application/commerce-api.ts +21 -21
  83. package/src/application/logging/__tests__/logger.test.ts +159 -0
  84. package/src/application/logging/index.ts +12 -0
  85. package/src/application/logging/logLevel.ts +162 -0
  86. package/src/application/logging/logger.ts +13 -0
  87. package/src/contexts/salesAgentContext/salesAgentService.ts +1 -1
  88. package/src/contexts/userIdentityContext/__tests__/userIdentityContext.test.tsx +4 -5
  89. package/src/hooks/ChatToggle/__tests__/useChatToggle.test.ts +273 -0
  90. package/src/hooks/ChatToggle/useChatToggle.ts +39 -12
  91. package/src/hooks/Intersection/useIntersection.ts +7 -4
  92. package/src/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.ts +8 -5
  93. package/src/hooks/WidgetInteraction/__tests__/chatTriggerWidget.test.ts +51 -0
  94. package/src/hooks/WidgetInteraction/types.ts +53 -0
  95. package/src/services/amplitudeService/__tests__/amplitudeService.test.ts +76 -0
  96. 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
- // TODO this function is recreated every time new data comes from the stream - define it outside of the generator
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
- line,
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
- // TODO this function is recreated every time new data comes from the stream - define it outside of the generator
283
- const processChunk = (chunk: string): unknown[] => {
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); // and filter out the undefined values
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
- // Massage and parse the chunk of data
304
- const chunk = decoder.decode(value);
305
- const parsedLines = processChunk(chunk);
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 parsedLines) {
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
- console.error('error getting streaming responses', e);
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')).toBeInTheDocument();
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', () => {