@envive-ai/react-hooks 0.3.19-alpha-marlo-4 → 0.3.19-alpha-marlo-6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envive-ai/react-hooks",
3
- "version": "0.3.19-alpha-marlo-4",
3
+ "version": "0.3.19-alpha-marlo-6",
4
4
  "description": "React hooks for connecting to Envive AI services.",
5
5
  "keywords": [
6
6
  "react",
@@ -332,7 +332,7 @@ describe('AmplitudeProvider - React Context Integration', () => {
332
332
  });
333
333
  });
334
334
 
335
- it('should still render children when not ready', async () => {
335
+ it('should render children with isReady=false when amplitude key is missing', async () => {
336
336
  // Create a provider without required dependencies
337
337
  const IncompleteWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
338
338
  return (
@@ -357,13 +357,14 @@ describe('AmplitudeProvider - React Context Integration', () => {
357
357
 
358
358
  render(
359
359
  <IncompleteWrapper>
360
- <div data-testid="should-render">Should render</div>
360
+ <MockAmplitudeComponent />
361
361
  </IncompleteWrapper>,
362
362
  );
363
363
 
364
- // AmplitudeProvider always renders children, even when not ready
364
+ // AmplitudeProvider renders children even when not ready — isReady reflects tracking unavailability
365
365
  await waitFor(() => {
366
- expect(screen.getByTestId('should-render')).toBeInTheDocument();
366
+ expect(screen.getByTestId('amplitude-component')).toBeInTheDocument();
367
+ expect(screen.getByTestId('is-ready').textContent).toBe('false');
367
368
  });
368
369
  });
369
370
  });
@@ -446,11 +447,15 @@ describe('AmplitudeProvider - React Context Integration', () => {
446
447
  });
447
448
 
448
449
  describe('useAmplitude hook', () => {
449
- it('should return a no-op context when used outside provider', () => {
450
- render(<MockAmplitudeComponent />);
450
+ it('should throw error when used outside provider', () => {
451
+ // Suppress console.error for this test
452
+ const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
453
+
454
+ expect(() => {
455
+ render(<MockAmplitudeComponent />);
456
+ }).toThrow('useAmplitude must be used within AmplitudeProvider');
451
457
 
452
- expect(screen.getByTestId('amplitude-component')).toBeInTheDocument();
453
- expect(screen.getByTestId('is-ready').textContent).toBe('false');
458
+ consoleError.mockRestore();
454
459
  });
455
460
 
456
461
  it('should provide amplitude context when used within provider', async () => {
@@ -26,13 +26,7 @@ interface AmplitudeContextType {
26
26
  setSupplementalDefaultProps: (props: Record<string, unknown>) => void;
27
27
  }
28
28
 
29
- const noOpAmplitudeContext: AmplitudeContextType = {
30
- trackEvent: async () => {},
31
- isReady: false,
32
- setSupplementalDefaultProps: () => {},
33
- };
34
-
35
- const AmplitudeContext = createContext<AmplitudeContextType>(noOpAmplitudeContext);
29
+ const AmplitudeContext = createContext<AmplitudeContextType | null>(null);
36
30
 
37
31
  export const AmplitudeProvider: React.FC<{
38
32
  externalAmplitudeService?: AmplitudeService;
@@ -124,13 +118,17 @@ export const AmplitudeProvider: React.FC<{
124
118
  [service, isReady],
125
119
  );
126
120
 
127
- return (
128
- <AmplitudeContext.Provider value={isReady ? value : noOpAmplitudeContext}>
129
- {children}
130
- </AmplitudeContext.Provider>
131
- );
121
+ if (!isReady) {
122
+ return null;
123
+ }
124
+
125
+ return <AmplitudeContext.Provider value={value}>{children}</AmplitudeContext.Provider>;
132
126
  };
133
127
 
134
128
  export const useAmplitude = () => {
135
- return useContext(AmplitudeContext);
129
+ const context = useContext(AmplitudeContext);
130
+ if (!context) {
131
+ throw new Error('useAmplitude must be used within AmplitudeProvider');
132
+ }
133
+ return context;
136
134
  };
@@ -85,10 +85,12 @@ describe('AmplitudeService', () => {
85
85
  });
86
86
  });
87
87
 
88
+ const validApiKey = 'abcdef1234567890abcdef1234567890';
89
+
88
90
  const createService = (overrides?: Partial<AmplitudeServiceConfig>) => {
89
91
  return new AmplitudeService({
90
92
  userId: 'test-user-id',
91
- amplitudeApiKey: 'test-api-key',
93
+ amplitudeApiKey: validApiKey,
92
94
  dataResidency: 'US',
93
95
  env: 'test',
94
96
  orgId: 'test-org-id',
@@ -107,7 +109,7 @@ describe('AmplitudeService', () => {
107
109
  createService();
108
110
 
109
111
  expect(mockInit).toHaveBeenCalledWith(
110
- 'test-api-key',
112
+ validApiKey,
111
113
  'test-user-id',
112
114
  expect.objectContaining({
113
115
  serverZone: 'US',
@@ -121,7 +123,7 @@ describe('AmplitudeService', () => {
121
123
  it('should not be ready if userId is missing', () => {
122
124
  const service = new AmplitudeService({
123
125
  userId: '',
124
- amplitudeApiKey: 'test-api-key',
126
+ amplitudeApiKey: validApiKey,
125
127
  dataResidency: 'US',
126
128
  env: 'test',
127
129
  orgId: 'test-org-id',
@@ -157,7 +159,7 @@ describe('AmplitudeService', () => {
157
159
  it('should not be ready if featureFlagService is missing', () => {
158
160
  const service = new AmplitudeService({
159
161
  userId: 'test-user-id',
160
- amplitudeApiKey: 'test-api-key',
162
+ amplitudeApiKey: validApiKey,
161
163
  dataResidency: 'US',
162
164
  env: 'test',
163
165
  orgId: 'test-org-id',
@@ -177,6 +179,54 @@ describe('AmplitudeService', () => {
177
179
 
178
180
  expect(service.isReady).toBe(true);
179
181
  });
182
+
183
+ it('should not set isMockMode when all required config is provided', () => {
184
+ const service = createService();
185
+
186
+ expect(service.isMockMode).toBe(false);
187
+ });
188
+ });
189
+
190
+ describe('Mock mode', () => {
191
+ it('should set isMockMode and not initialize the client when API key is not a valid hex string', () => {
192
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
193
+
194
+ const service = createService({ amplitudeApiKey: 'not-a-valid-key' });
195
+
196
+ expect(service.isReady).toBe(false);
197
+ expect(service.isMockMode).toBe(true);
198
+ expect(mockInit).not.toHaveBeenCalled();
199
+
200
+ consoleSpy.mockRestore();
201
+ });
202
+
203
+ it('should print a console.log message when a mock API key is detected', () => {
204
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
205
+
206
+ createService({ amplitudeApiKey: 'not-a-valid-key' });
207
+
208
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Mock API key detected'));
209
+
210
+ consoleSpy.mockRestore();
211
+ });
212
+
213
+ it('should not set isMockMode when API key is empty — service is simply not ready', () => {
214
+ const service = createService({ amplitudeApiKey: '' });
215
+
216
+ expect(service.isReady).toBe(false);
217
+ expect(service.isMockMode).toBe(false);
218
+ });
219
+
220
+ it('should not call the Amplitude client when trackEvent is called in mock mode', async () => {
221
+ const service = createService({ amplitudeApiKey: 'not-a-valid-key' });
222
+
223
+ await service.trackEvent({
224
+ eventName: SpiffyMetricsEventName.ChatUserMessageInput,
225
+ eventProps: { message: 'test' },
226
+ });
227
+
228
+ expect(mockTrack).not.toHaveBeenCalled();
229
+ });
180
230
  });
181
231
 
182
232
  describe('trackEvent', () => {
@@ -269,7 +319,7 @@ describe('AmplitudeService', () => {
269
319
  it('should not track if client is not initialized', async () => {
270
320
  const service = new AmplitudeService({
271
321
  userId: '',
272
- amplitudeApiKey: 'test-api-key',
322
+ amplitudeApiKey: validApiKey,
273
323
  dataResidency: 'US',
274
324
  env: 'test',
275
325
  orgId: 'test-org-id',
@@ -419,7 +469,7 @@ describe('AmplitudeService', () => {
419
469
  it('should fall back to window.localStorage when getLocalStorageItem is not provided', async () => {
420
470
  const service = new AmplitudeService({
421
471
  userId: 'test-user-id',
422
- amplitudeApiKey: 'test-api-key',
472
+ amplitudeApiKey: validApiKey,
423
473
  dataResidency: 'US',
424
474
  env: 'test',
425
475
  orgId: 'test-org-id',
@@ -51,6 +51,8 @@ export class AmplitudeService {
51
51
 
52
52
  private config: AmplitudeServiceConfig;
53
53
 
54
+ private _isMockMode: boolean = false;
55
+
54
56
  constructor(config: AmplitudeServiceConfig) {
55
57
  this.config = config;
56
58
  this.initialize();
@@ -65,6 +67,10 @@ export class AmplitudeService {
65
67
  );
66
68
  }
67
69
 
70
+ get isMockMode(): boolean {
71
+ return this._isMockMode;
72
+ }
73
+
68
74
  private getLocalStorageItem(key: string): string | null {
69
75
  if (this.config.getLocalStorageItem) {
70
76
  return this.config.getLocalStorageItem(key);
@@ -213,8 +219,8 @@ export class AmplitudeService {
213
219
  return enrichment;
214
220
  }
215
221
 
216
- static isDummyApiKey(apiKey: string): boolean {
217
- return !/^[0-9a-f]{32}$/i.test(apiKey);
222
+ private static isValidAmplitudeApiKey(apiKey: string): boolean {
223
+ return /^[0-9a-f]{32}$/i.test(apiKey);
218
224
  }
219
225
 
220
226
  private initialize(): void {
@@ -232,8 +238,12 @@ export class AmplitudeService {
232
238
  return;
233
239
  }
234
240
 
235
- if (AmplitudeService.isDummyApiKey(this.config.amplitudeApiKey)) {
236
- logger.logDebug('AmplitudeService is using a dummy API key — analytics will not be tracked');
241
+ if (!AmplitudeService.isValidAmplitudeApiKey(this.config.amplitudeApiKey)) {
242
+ this._isMockMode = true;
243
+ // eslint-disable-next-line no-console
244
+ console.log(
245
+ '[AmplitudeService] Mock API key detected — running in mock mode, no events will be sent to Amplitude.',
246
+ );
237
247
  return;
238
248
  }
239
249