@envive-ai/react-hooks 0.2.7-arthur-1 → 0.2.7
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/{TrackComponentVisibleEvent-CXhKOwKQ.js → TrackComponentVisibleEvent-qNE0dIL1.js} +2 -2
- package/dist/{TrackComponentVisibleEvent-CgxCqrIt.cjs → TrackComponentVisibleEvent-tDRnrJi3.cjs} +2 -2
- package/dist/amplitudeContext-C6LHpqP_.cjs +290 -0
- package/dist/amplitudeContext-rEjGf57q.js +268 -0
- package/dist/application/utils/index.cjs +2 -2
- package/dist/application/utils/index.js +2 -2
- package/dist/atoms/app/index.d.cts +7 -7
- package/dist/atoms/app/index.d.ts +1 -1
- package/dist/atoms/chat/index.cjs +3 -3
- package/dist/atoms/chat/index.d.cts +26 -26
- package/dist/atoms/chat/index.d.ts +1 -1
- package/dist/atoms/chat/index.js +3 -3
- package/dist/atoms/globalSearch/index.d.cts +6 -6
- package/dist/atoms/globalSearch/index.d.ts +5 -5
- package/dist/atoms/org/index.d.cts +16 -16
- package/dist/atoms/org/index.d.ts +16 -16
- package/dist/atoms/search/index.cjs +5 -5
- package/dist/atoms/search/index.js +5 -5
- package/dist/atoms/search/utils.d.ts +1 -1
- package/dist/{cdnContext-CtrIlAqX.js → cdnContext-B8zWuGGT.js} +1 -1
- package/dist/{cdnContext-CaDyQ_5p.cjs → cdnContext-DzifgoNo.cjs} +1 -1
- package/dist/{chat-BjhQCyW_.js → chat-DChvXHjz.js} +3 -3
- package/dist/{chat-BkPax29G.cjs → chat-L_N0qaqs.cjs} +3 -3
- package/dist/{chatSearch-C3N3iIxu.cjs → chatSearch-B5whqPLW.cjs} +3 -3
- package/dist/{chatSearch-BsYlFvpv.js → chatSearch-so-qeiEL.js} +3 -3
- package/dist/{chatState-CJ52Ag_7.cjs → chatState-D9_aA1_h.cjs} +2 -2
- package/dist/{chatState-CXA1vF16.js → chatState-Dl5lyuKC.js} +2 -2
- package/dist/{commerce-api-rgj30eEp.js → commerce-api-DEFd5HUH.js} +2 -2
- package/dist/{commerce-api-DA1QGGMK.cjs → commerce-api-DqVD4NH8.cjs} +2 -2
- package/dist/contexts/amplitudeContext/index.cjs +2 -2
- package/dist/contexts/amplitudeContext/index.js +2 -2
- package/dist/contexts/cdnContext/index.cjs +1 -1
- package/dist/contexts/cdnContext/index.js +1 -1
- package/dist/contexts/chatContext/index.cjs +7 -7
- package/dist/contexts/chatContext/index.d.ts +4 -4
- package/dist/contexts/chatContext/index.js +7 -7
- package/dist/contexts/systemSettingsContext/index.d.cts +2 -2
- package/dist/contexts/systemSettingsContext/index.d.ts +4 -4
- package/dist/contexts/userIdentityContext/index.cjs +4 -4
- package/dist/contexts/userIdentityContext/index.js +4 -4
- package/dist/frontendConfig-Cawh5iqv.d.ts +1 -1
- package/dist/frontendConfig-iZipB5FG.d.cts +1 -1
- package/dist/hooks/AmplitudeOperations/index.cjs +3 -3
- package/dist/hooks/AmplitudeOperations/index.js +3 -3
- package/dist/hooks/CdnOperations/index.cjs +1 -1
- package/dist/hooks/CdnOperations/index.js +1 -1
- package/dist/hooks/ChatToggle/index.cjs +3 -3
- package/dist/hooks/ChatToggle/index.js +3 -3
- package/dist/hooks/ChatToggleAnalytics/index.cjs +4 -4
- package/dist/hooks/ChatToggleAnalytics/index.js +4 -4
- package/dist/hooks/GrabAndScroll/index.d.cts +2 -2
- package/dist/hooks/IdentifyUser/index.cjs +4 -4
- package/dist/hooks/IdentifyUser/index.js +4 -4
- package/dist/hooks/Search/index.cjs +19 -1157
- package/dist/hooks/Search/index.js +17 -1156
- package/dist/hooks/SystemSettingsContext/index.d.ts +2 -2
- package/dist/hooks/TrackComponentVisibleEvent/index.cjs +2 -2
- package/dist/hooks/TrackComponentVisibleEvent/index.js +2 -2
- package/dist/hooks/UpdateAnalyticsProps/index.cjs +1 -1
- package/dist/hooks/UpdateAnalyticsProps/index.js +1 -1
- package/dist/{search-Csh2n66W.cjs → search-CneZkauD.cjs} +2 -2
- package/dist/{search-DkiqkogN.js → search-DbZeJYdg.js} +2 -2
- package/dist/{useAmplitudeOperations-zIRSqmMW.js → useAmplitudeOperations-B9HjVe6G.js} +2 -2
- package/dist/{useAmplitudeOperations-Bo6YNbTV.cjs → useAmplitudeOperations-BdvxAzuI.cjs} +2 -2
- package/dist/userIdentityContext-CPztaX6-.js +126 -0
- package/dist/userIdentityContext-DdySlQBz.cjs +143 -0
- package/dist/{utils-C1ErYSoW.js → utils-B4PvsKVY.js} +2 -2
- package/dist/{utils-mqfncrhI.cjs → utils-BRVP7Mnu.cjs} +2 -2
- package/dist/utils-CBD4g2Nc.d.cts +1 -1
- package/dist/utils-QKFAbPT6.d.ts +1 -1
- package/package.json +3 -2
- package/src/contexts/amplitudeContext/__tests__/amplitudeContext.test.tsx +525 -0
- package/src/contexts/amplitudeContext/amplitudeContext.tsx +6 -2
- package/src/contexts/userIdentityContext/__tests__/userIdentityContext.test.tsx +0 -162
- package/src/contexts/userIdentityContext/userIdentityContext.tsx +13 -4
- package/dist/amplitudeContext-C8tT74Mi.cjs +0 -286
- package/dist/amplitudeContext-DCk6Va-j.js +0 -264
- package/dist/userIdentityContext-BqbNu7xu.cjs +0 -132
- package/dist/userIdentityContext-BxFH9FNQ.js +0 -115
- /package/dist/{AmplitudeOperations-ChZWcSsc.js → AmplitudeOperations-C-ieCm9m.js} +0 -0
- /package/dist/{AmplitudeOperations-JggIc1zD.cjs → AmplitudeOperations-p7APchq9.cjs} +0 -0
- /package/dist/{amplitudeContext-BItT9HmT.js → amplitudeContext-DbicJUzl.js} +0 -0
- /package/dist/{amplitudeContext-DPtyVv3Q.cjs → amplitudeContext-q3mggFSE.cjs} +0 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen, waitFor, act } from "@testing-library/react";
|
|
3
|
+
import { Provider, useStore } from "jotai";
|
|
4
|
+
import {
|
|
5
|
+
AmplitudeProvider,
|
|
6
|
+
useAmplitude,
|
|
7
|
+
SpiffyMetricsEventName,
|
|
8
|
+
} from "../amplitudeContext";
|
|
9
|
+
import {
|
|
10
|
+
UserIdentityProvider,
|
|
11
|
+
useUserIdentity,
|
|
12
|
+
} from "src/contexts/userIdentityContext/userIdentityContext";
|
|
13
|
+
import { LocalStorageProvider } from "src/contexts/localStorageContext";
|
|
14
|
+
import { EnviveConfigProvider } from "src/contexts/enviveConfigContext/enviveConfigContext";
|
|
15
|
+
import { FeatureFlagServiceProvider } from "src/contexts/featureFlagServiceContext/featureFlagServiceContext";
|
|
16
|
+
import { userIdAtom } from "src/atoms/app";
|
|
17
|
+
import Logger from "src/application/logging/logger";
|
|
18
|
+
import { createInstance } from "@amplitude/analytics-browser";
|
|
19
|
+
|
|
20
|
+
// Mock the Logger to avoid console output in tests
|
|
21
|
+
// vi.spyOn(Logger, "logInfo").mockImplementation(() => {});
|
|
22
|
+
// vi.spyOn(Logger, "logWarn").mockImplementation(() => {});
|
|
23
|
+
// vi.spyOn(Logger, "logError").mockImplementation(() => {});
|
|
24
|
+
// vi.spyOn(Logger, "logDebug").mockImplementation(() => {});
|
|
25
|
+
|
|
26
|
+
// Mock Amplitude
|
|
27
|
+
const mockTrack = vi.fn();
|
|
28
|
+
const mockInit = vi.fn();
|
|
29
|
+
const mockAdd = vi.fn();
|
|
30
|
+
|
|
31
|
+
vi.mock("@amplitude/analytics-browser", () => ({
|
|
32
|
+
createInstance: vi.fn(() => ({
|
|
33
|
+
track: mockTrack,
|
|
34
|
+
init: mockInit,
|
|
35
|
+
add: mockAdd,
|
|
36
|
+
})),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
// Mock EventsDispatcher
|
|
40
|
+
vi.mock("src/events", () => ({
|
|
41
|
+
EventsDispatcher: {
|
|
42
|
+
dispatch: vi.fn(),
|
|
43
|
+
},
|
|
44
|
+
SpiffyEvent: {
|
|
45
|
+
AMPLITUDE_EVENT: "AMPLITUDE_EVENT",
|
|
46
|
+
},
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
// Mock crypto.subtle.digest for insert_id generation
|
|
50
|
+
const mockDigest = vi.fn().mockResolvedValue(new Uint8Array(32).fill(0));
|
|
51
|
+
Object.defineProperty(global, "crypto", {
|
|
52
|
+
value: {
|
|
53
|
+
subtle: {
|
|
54
|
+
digest: mockDigest,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
writable: true,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Component that uses the useAmplitude hook
|
|
61
|
+
const MockAmplitudeComponent: React.FC = () => {
|
|
62
|
+
const amplitude = useAmplitude();
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div data-testid="amplitude-component">
|
|
66
|
+
<div data-testid="is-ready">{amplitude.isReady.toString()}</div>
|
|
67
|
+
<button
|
|
68
|
+
data-testid="track-event-button"
|
|
69
|
+
onClick={() =>
|
|
70
|
+
amplitude.trackEvent({
|
|
71
|
+
eventName: SpiffyMetricsEventName.ChatUserMessageInput,
|
|
72
|
+
eventProps: { message: "test message" },
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
>
|
|
76
|
+
Track Event
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Component that reads userIdAtom to verify it's set
|
|
83
|
+
const AtomReaderComponent: React.FC = () => {
|
|
84
|
+
const store = useStore();
|
|
85
|
+
const [userId, setUserId] = React.useState<string>("not-set");
|
|
86
|
+
|
|
87
|
+
React.useEffect(() => {
|
|
88
|
+
const unsubscribe = store.sub(userIdAtom, () => {
|
|
89
|
+
try {
|
|
90
|
+
const value = store.get(userIdAtom);
|
|
91
|
+
if (value) {
|
|
92
|
+
setUserId(value);
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// Still not set
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const timer = setTimeout(() => {
|
|
100
|
+
try {
|
|
101
|
+
const value = store.get(userIdAtom);
|
|
102
|
+
if (value) {
|
|
103
|
+
setUserId(value);
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// Not set yet
|
|
107
|
+
}
|
|
108
|
+
}, 50);
|
|
109
|
+
|
|
110
|
+
return () => {
|
|
111
|
+
unsubscribe();
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
};
|
|
114
|
+
}, [store]);
|
|
115
|
+
|
|
116
|
+
return <div data-testid="atom-reader">{userId}</div>;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Component that uses both contexts
|
|
120
|
+
const CombinedContextComponent: React.FC = () => {
|
|
121
|
+
const userIdentity = useUserIdentity();
|
|
122
|
+
const amplitude = useAmplitude();
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div data-testid="combined-component">
|
|
126
|
+
<div data-testid="user-id-from-identity">
|
|
127
|
+
{userIdentity.getUserIdOrDefault()}
|
|
128
|
+
</div>
|
|
129
|
+
<div data-testid="amplitude-ready">{amplitude.isReady.toString()}</div>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Wrapper component with all required providers
|
|
135
|
+
const TestWrapper: React.FC<{
|
|
136
|
+
children: React.ReactNode;
|
|
137
|
+
amplitudeApiKey?: string;
|
|
138
|
+
userIdOverride?: string;
|
|
139
|
+
userIdDefault?: string;
|
|
140
|
+
}> = ({
|
|
141
|
+
children,
|
|
142
|
+
amplitudeApiKey = "test-amplitude-key",
|
|
143
|
+
userIdOverride,
|
|
144
|
+
userIdDefault,
|
|
145
|
+
}) => {
|
|
146
|
+
// Set up localStorage with user IDs if provided
|
|
147
|
+
React.useEffect(() => {
|
|
148
|
+
if (userIdOverride) {
|
|
149
|
+
localStorage.setItem("v1-spiffy-user-id-override", userIdOverride);
|
|
150
|
+
}
|
|
151
|
+
if (userIdDefault) {
|
|
152
|
+
localStorage.setItem("v1-spiffy-user-id-default", userIdDefault);
|
|
153
|
+
}
|
|
154
|
+
}, [userIdOverride, userIdDefault]);
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<Provider>
|
|
158
|
+
<EnviveConfigProvider
|
|
159
|
+
identifyingPrefix="test"
|
|
160
|
+
orgShortName="test-org"
|
|
161
|
+
amplitudeApiKey={amplitudeApiKey}
|
|
162
|
+
dataResidency="US"
|
|
163
|
+
env="test"
|
|
164
|
+
contextSource="app"
|
|
165
|
+
featureGates={[]}
|
|
166
|
+
>
|
|
167
|
+
<LocalStorageProvider>
|
|
168
|
+
<UserIdentityProvider>
|
|
169
|
+
<FeatureFlagServiceProvider featureGates={[]}>
|
|
170
|
+
<AmplitudeProvider>{children}</AmplitudeProvider>
|
|
171
|
+
</FeatureFlagServiceProvider>
|
|
172
|
+
</UserIdentityProvider>
|
|
173
|
+
</LocalStorageProvider>
|
|
174
|
+
</EnviveConfigProvider>
|
|
175
|
+
</Provider>
|
|
176
|
+
);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
describe("AmplitudeProvider with UserIdentityContext", () => {
|
|
180
|
+
beforeEach(() => {
|
|
181
|
+
vi.clearAllMocks();
|
|
182
|
+
if (typeof localStorage !== "undefined") {
|
|
183
|
+
localStorage.clear();
|
|
184
|
+
}
|
|
185
|
+
// Reset mocks
|
|
186
|
+
mockTrack.mockClear();
|
|
187
|
+
mockInit.mockClear();
|
|
188
|
+
mockAdd.mockClear();
|
|
189
|
+
mockDigest.mockResolvedValue(new Uint8Array(32).fill(0));
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("Integration with UserIdentityContext - userIdAtom", () => {
|
|
193
|
+
it("should be ready when UserIdentityContext provides a userId", async () => {
|
|
194
|
+
render(
|
|
195
|
+
<TestWrapper>
|
|
196
|
+
<CombinedContextComponent />
|
|
197
|
+
</TestWrapper>
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
await waitFor(() => {
|
|
201
|
+
const isReady = screen.getByTestId("amplitude-ready").textContent;
|
|
202
|
+
expect(isReady).toBe("true");
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("should use userId from UserIdentityContext in tracking events", async () => {
|
|
207
|
+
const testUserId = "test-user-id-123";
|
|
208
|
+
render(
|
|
209
|
+
<TestWrapper userIdOverride={testUserId}>
|
|
210
|
+
<MockAmplitudeComponent />
|
|
211
|
+
</TestWrapper>
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
await waitFor(() => {
|
|
215
|
+
expect(screen.getByTestId("is-ready").textContent).toBe("true");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
await act(async () => {
|
|
219
|
+
screen.getByTestId("track-event-button").click();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
await waitFor(() => {
|
|
223
|
+
expect(mockTrack).toHaveBeenCalled();
|
|
224
|
+
const callArgs = mockTrack.mock.calls[0];
|
|
225
|
+
expect(callArgs[0]).toBe("[Spiffy] Chat User Message Input");
|
|
226
|
+
expect(callArgs[1]).toHaveProperty("user.id", testUserId);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("should use default userId from UserIdentityContext when override is not set", async () => {
|
|
231
|
+
const testUserId = "default-user-id-456";
|
|
232
|
+
render(
|
|
233
|
+
<TestWrapper userIdDefault={testUserId}>
|
|
234
|
+
<MockAmplitudeComponent />
|
|
235
|
+
</TestWrapper>
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
await waitFor(() => {
|
|
239
|
+
expect(screen.getByTestId("is-ready").textContent).toBe("true");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await act(async () => {
|
|
243
|
+
screen.getByTestId("track-event-button").click();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
await waitFor(() => {
|
|
247
|
+
expect(mockTrack).toHaveBeenCalled();
|
|
248
|
+
const callArgs = mockTrack.mock.calls[0];
|
|
249
|
+
expect(callArgs[1]).toHaveProperty("user.id", testUserId);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("should prioritize override userId over default userId", async () => {
|
|
254
|
+
const overrideUserId = "override-user-id-789";
|
|
255
|
+
const defaultUserId = "default-user-id-012";
|
|
256
|
+
render(
|
|
257
|
+
<TestWrapper
|
|
258
|
+
userIdOverride={overrideUserId}
|
|
259
|
+
userIdDefault={defaultUserId}
|
|
260
|
+
>
|
|
261
|
+
<MockAmplitudeComponent />
|
|
262
|
+
</TestWrapper>
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
await waitFor(() => {
|
|
266
|
+
expect(screen.getByTestId("is-ready").textContent).toBe("true");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
await act(async () => {
|
|
270
|
+
screen.getByTestId("track-event-button").click();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
await waitFor(() => {
|
|
274
|
+
expect(mockTrack).toHaveBeenCalled();
|
|
275
|
+
const callArgs = mockTrack.mock.calls[0];
|
|
276
|
+
expect(callArgs[1]).toHaveProperty("user.id", overrideUserId);
|
|
277
|
+
expect(callArgs[1]).not.toHaveProperty("user.id", defaultUserId);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("should initialize Amplitude with userId from UserIdentityContext", async () => {
|
|
282
|
+
const testUserId = "init-user-id-345";
|
|
283
|
+
render(
|
|
284
|
+
<TestWrapper userIdOverride={testUserId}>
|
|
285
|
+
<MockAmplitudeComponent />
|
|
286
|
+
</TestWrapper>
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
await waitFor(() => {
|
|
290
|
+
expect(mockInit).toHaveBeenCalled();
|
|
291
|
+
const initArgs = mockInit.mock.calls[0];
|
|
292
|
+
expect(initArgs[1]).toBe(testUserId);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe("userIdAtom synchronization", () => {
|
|
298
|
+
it("should sync userIdAtom with UserIdentityContext userId", async () => {
|
|
299
|
+
const testUserId = "sync-user-id-678";
|
|
300
|
+
render(
|
|
301
|
+
<TestWrapper userIdOverride={testUserId}>
|
|
302
|
+
<>
|
|
303
|
+
<CombinedContextComponent />
|
|
304
|
+
<AtomReaderComponent />
|
|
305
|
+
</>
|
|
306
|
+
</TestWrapper>
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
await waitFor(() => {
|
|
310
|
+
const identityUserId = screen.getByTestId(
|
|
311
|
+
"user-id-from-identity"
|
|
312
|
+
).textContent;
|
|
313
|
+
const atomUserId = screen.getByTestId("atom-reader").textContent;
|
|
314
|
+
expect(identityUserId).toBe(testUserId);
|
|
315
|
+
expect(atomUserId).toBe(testUserId);
|
|
316
|
+
expect(identityUserId).toBe(atomUserId);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe("Event tracking with UserIdentityContext userId", () => {
|
|
322
|
+
it("should include user.id in all tracked events", async () => {
|
|
323
|
+
const testUserId = "event-user-id-901";
|
|
324
|
+
render(
|
|
325
|
+
<TestWrapper userIdOverride={testUserId}>
|
|
326
|
+
<MockAmplitudeComponent />
|
|
327
|
+
</TestWrapper>
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
await waitFor(() => {
|
|
331
|
+
expect(screen.getByTestId("is-ready").textContent).toBe("true");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
await act(async () => {
|
|
335
|
+
screen.getByTestId("track-event-button").click();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
await waitFor(() => {
|
|
339
|
+
expect(mockTrack).toHaveBeenCalled();
|
|
340
|
+
const callArgs = mockTrack.mock.calls[0];
|
|
341
|
+
expect(callArgs[1]).toHaveProperty("user.id", testUserId);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("should include user.id in default tracking props", async () => {
|
|
346
|
+
const testUserId = "default-props-user-id";
|
|
347
|
+
render(
|
|
348
|
+
<TestWrapper userIdOverride={testUserId}>
|
|
349
|
+
<MockAmplitudeComponent />
|
|
350
|
+
</TestWrapper>
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
await waitFor(() => {
|
|
354
|
+
expect(screen.getByTestId("is-ready").textContent).toBe("true");
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
await act(async () => {
|
|
358
|
+
screen.getByTestId("track-event-button").click();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
await waitFor(() => {
|
|
362
|
+
expect(mockTrack).toHaveBeenCalled();
|
|
363
|
+
const callArgs = mockTrack.mock.calls[0];
|
|
364
|
+
const eventProps = callArgs[1];
|
|
365
|
+
expect(eventProps["user.id"]).toBe(testUserId);
|
|
366
|
+
expect(eventProps["cdp.user_id"]).toBe(null);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("should track multiple events with correct userId", async () => {
|
|
371
|
+
const testUserId = "multi-event-user-id";
|
|
372
|
+
const MultiEventComponent: React.FC = () => {
|
|
373
|
+
const amplitude = useAmplitude();
|
|
374
|
+
|
|
375
|
+
const trackMultiple = () => {
|
|
376
|
+
amplitude.trackEvent({
|
|
377
|
+
eventName: SpiffyMetricsEventName.ChatUserMessageInput,
|
|
378
|
+
eventProps: { event1: true },
|
|
379
|
+
});
|
|
380
|
+
amplitude.trackEvent({
|
|
381
|
+
eventName: SpiffyMetricsEventName.ChatSuggestionClicked,
|
|
382
|
+
eventProps: { event2: true },
|
|
383
|
+
});
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
return (
|
|
387
|
+
<button data-testid="track-multiple" onClick={trackMultiple}>
|
|
388
|
+
Track Multiple
|
|
389
|
+
</button>
|
|
390
|
+
);
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
render(
|
|
394
|
+
<TestWrapper userIdOverride={testUserId}>
|
|
395
|
+
<MultiEventComponent />
|
|
396
|
+
</TestWrapper>
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
await waitFor(() => {
|
|
400
|
+
expect(screen.getByTestId("track-multiple")).toBeInTheDocument();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
await act(async () => {
|
|
404
|
+
screen.getByTestId("track-multiple").click();
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
await waitFor(() => {
|
|
408
|
+
expect(mockTrack).toHaveBeenCalledTimes(2);
|
|
409
|
+
mockTrack.mock.calls.forEach((call: any[]) => {
|
|
410
|
+
expect(call[1]).toHaveProperty("user.id", testUserId);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
describe("Context readiness and initialization", () => {
|
|
417
|
+
it("should wait for UserIdentityContext to be ready before becoming ready", async () => {
|
|
418
|
+
render(
|
|
419
|
+
<TestWrapper>
|
|
420
|
+
<CombinedContextComponent />
|
|
421
|
+
</TestWrapper>
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
// Both contexts should eventually be ready
|
|
425
|
+
await waitFor(() => {
|
|
426
|
+
const amplitudeReady = screen.getByTestId("amplitude-ready").textContent;
|
|
427
|
+
expect(amplitudeReady).toBe("true");
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("should initialize Amplitude client with correct userId from UserIdentityContext", async () => {
|
|
432
|
+
const testUserId = "init-client-user-id";
|
|
433
|
+
render(
|
|
434
|
+
<TestWrapper userIdOverride={testUserId}>
|
|
435
|
+
<MockAmplitudeComponent />
|
|
436
|
+
</TestWrapper>
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
await waitFor(() => {
|
|
440
|
+
expect(mockInit).toHaveBeenCalledWith(
|
|
441
|
+
"test-amplitude-key",
|
|
442
|
+
testUserId,
|
|
443
|
+
expect.objectContaining({
|
|
444
|
+
serverZone: "US",
|
|
445
|
+
})
|
|
446
|
+
);
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it("should not render children when not ready", async () => {
|
|
451
|
+
// Create a provider without required dependencies
|
|
452
|
+
const IncompleteWrapper: React.FC<{ children: React.ReactNode }> = ({
|
|
453
|
+
children,
|
|
454
|
+
}) => {
|
|
455
|
+
return (
|
|
456
|
+
<Provider>
|
|
457
|
+
<EnviveConfigProvider
|
|
458
|
+
identifyingPrefix="test"
|
|
459
|
+
orgShortName="test-org"
|
|
460
|
+
// Missing amplitudeApiKey
|
|
461
|
+
featureGates={[]}
|
|
462
|
+
>
|
|
463
|
+
<LocalStorageProvider>
|
|
464
|
+
<UserIdentityProvider>
|
|
465
|
+
<FeatureFlagServiceProvider featureGates={[]}>
|
|
466
|
+
<AmplitudeProvider>{children}</AmplitudeProvider>
|
|
467
|
+
</FeatureFlagServiceProvider>
|
|
468
|
+
</UserIdentityProvider>
|
|
469
|
+
</LocalStorageProvider>
|
|
470
|
+
</EnviveConfigProvider>
|
|
471
|
+
</Provider>
|
|
472
|
+
);
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const { container } = render(
|
|
476
|
+
<IncompleteWrapper>
|
|
477
|
+
<div data-testid="should-not-render">Should not render</div>
|
|
478
|
+
</IncompleteWrapper>
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
// AmplitudeProvider returns null when not ready, so children should not render
|
|
482
|
+
await waitFor(() => {
|
|
483
|
+
expect(
|
|
484
|
+
screen.queryByTestId("should-not-render")
|
|
485
|
+
).not.toBeInTheDocument();
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
describe("Error handling", () => {
|
|
491
|
+
it("should handle tracking errors gracefully when userId is available", async () => {
|
|
492
|
+
const testUserId = "error-user-id";
|
|
493
|
+
mockTrack.mockImplementationOnce(() => {
|
|
494
|
+
throw new Error("Tracking error");
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
const logErrorSpy = vi.spyOn(Logger, "logError");
|
|
498
|
+
|
|
499
|
+
render(
|
|
500
|
+
<TestWrapper userIdOverride={testUserId}>
|
|
501
|
+
<MockAmplitudeComponent />
|
|
502
|
+
</TestWrapper>
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
await waitFor(() => {
|
|
506
|
+
expect(screen.getByTestId("is-ready").textContent).toBe("true");
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
await act(async () => {
|
|
510
|
+
screen.getByTestId("track-event-button").click();
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
await waitFor(() => {
|
|
514
|
+
expect(logErrorSpy).toHaveBeenCalledWith(
|
|
515
|
+
"[spiffy-ai] Error tracking event",
|
|
516
|
+
expect.any(Error),
|
|
517
|
+
expect.objectContaining({
|
|
518
|
+
eventName: SpiffyMetricsEventName.ChatUserMessageInput,
|
|
519
|
+
})
|
|
520
|
+
);
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
|
|
@@ -34,7 +34,6 @@ import { useEnviveConfig } from "src/contexts/enviveConfigContext/enviveConfigCo
|
|
|
34
34
|
import {
|
|
35
35
|
useFeatureFlagService,
|
|
36
36
|
} from "src/contexts/featureFlagServiceContext/featureFlagServiceContext";
|
|
37
|
-
import { useUserIdentity } from "src/contexts/userIdentityContext/userIdentityContext";
|
|
38
37
|
|
|
39
38
|
export enum SpiffyMetricsEventName {
|
|
40
39
|
BundleLoaded = "Bundle Loaded",
|
|
@@ -116,7 +115,7 @@ export const AmplitudeProvider: React.FC<{ children: React.ReactNode }> = ({
|
|
|
116
115
|
React.useState<Record<string, unknown>>({});
|
|
117
116
|
|
|
118
117
|
const isReady = Boolean(
|
|
119
|
-
userId && featureFlagService && amplitudeApiKey
|
|
118
|
+
userId && featureFlagService && amplitudeApiKey
|
|
120
119
|
);
|
|
121
120
|
|
|
122
121
|
const getDefaultTrackingProps = useCallback((): Record<string, unknown> => {
|
|
@@ -416,6 +415,11 @@ export const AmplitudeProvider: React.FC<{ children: React.ReactNode }> = ({
|
|
|
416
415
|
[trackEvent, isReady, setSupplementalDefaultProps]
|
|
417
416
|
);
|
|
418
417
|
|
|
418
|
+
if (!isReady) {
|
|
419
|
+
console.log("AmplitudeProvider is not ready", isReady, userId, featureFlagService, amplitudeApiKey);
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
|
|
419
423
|
return (
|
|
420
424
|
<AmplitudeContext.Provider value={value}>
|
|
421
425
|
{children}
|