-
Notifications
You must be signed in to change notification settings - Fork 528
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(insights): do not throw when sending event right after creating insights middleware #4575
Changes from 3 commits
97244a5
9effefd
1d3c977
fa7ea26
5ddedee
8b10f89
0add3dc
b3e0247
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -36,6 +36,30 @@ describe('insights', () => { | |
}; | ||
}; | ||
|
||
const createUmdTestEnvironment = () => { | ||
const { | ||
insightsClient, | ||
libraryLoadedAndProcessQueue, | ||
} = createInsightsUmdVersion(); | ||
const instantSearchInstance = createInstantSearch({ | ||
client: algoliasearch('myAppId', 'myApiKey'), | ||
}); | ||
const helper = algoliasearchHelper({} as SearchClient, ''); | ||
const getUserToken = () => { | ||
return (helper.state as any).userToken; | ||
}; | ||
instantSearchInstance.mainIndex = { | ||
getHelper: () => helper, | ||
} as Index; | ||
return { | ||
insightsClient, | ||
libraryLoadedAndProcessQueue, | ||
instantSearchInstance, | ||
helper, | ||
getUserToken, | ||
}; | ||
}; | ||
|
||
beforeEach(() => { | ||
warning.cache = {}; | ||
}); | ||
|
@@ -79,22 +103,48 @@ describe('insights', () => { | |
}); | ||
}); | ||
|
||
it('warns dev if userToken is set before creating the middleware', () => { | ||
const { insightsClient, instantSearchInstance } = createTestEnvironment(); | ||
insightsClient('setUserToken', 'abc'); | ||
expect(() => { | ||
createInsightsMiddleware({ | ||
insightsClient, | ||
})({ instantSearchInstance }); | ||
}) | ||
.toWarnDev(`[InstantSearch.js]: You set userToken before \`createInsightsMiddleware()\` and it is ignored. | ||
Please set the token after the \`createInsightsMiddleware()\` call. | ||
it('does not throw when an event is sent right after the creation in UMD', done => { | ||
const { | ||
insightsClient, | ||
libraryLoadedAndProcessQueue, | ||
instantSearchInstance, | ||
} = createUmdTestEnvironment(); | ||
|
||
const middleware = createInsightsMiddleware({ | ||
insightsClient, | ||
})({ instantSearchInstance }); | ||
middleware.subscribe(); | ||
|
||
createInsightsMiddleware({ /* ... */ }); | ||
setTimeout(() => { | ||
// When the library is loaded later, it consumes the queue and sends the event. | ||
const { algoliaAnalytics } = libraryLoadedAndProcessQueue(); | ||
expect(algoliaAnalytics.viewedObjectIDs).toHaveBeenCalledTimes(1); | ||
expect(algoliaAnalytics.viewedObjectIDs).toHaveBeenCalledWith({ | ||
eventName: 'Hits Viewed', | ||
index: '', | ||
objectIDs: ['1', '2'], | ||
}); | ||
done(); | ||
}, 200); | ||
|
||
expect(() => { | ||
instantSearchInstance.sendEventToInsights({ | ||
eventType: 'view', | ||
insightsMethod: 'viewedObjectIDs', | ||
payload: { | ||
eventName: 'Hits Viewed', | ||
index: '', | ||
objectIDs: ['1', '2'], | ||
}, | ||
widgetType: 'ais.hits', | ||
}); | ||
}).not.toThrow(); | ||
|
||
insightsClient('setUserToken', 'your-user-token'); | ||
// or | ||
aa('setUserToken', 'your-user-token');`); | ||
// The library is not loaded yet, so it stays in the queue. | ||
expect(insightsClient.queue[insightsClient.queue.length - 1]).toEqual([ | ||
'viewedObjectIDs', | ||
{ eventName: 'Hits Viewed', index: '', objectIDs: ['1', '2'] }, | ||
]); | ||
}); | ||
|
||
it('applies clickAnalytics', () => { | ||
|
@@ -153,7 +203,7 @@ aa('setUserToken', 'your-user-token');`); | |
expect(getUserToken()).toEqual(ANONYMOUS_TOKEN); | ||
}); | ||
|
||
it('ignores userToken set before init', () => { | ||
it('applies userToken which was set before init', () => { | ||
const { | ||
insightsClient, | ||
instantSearchInstance, | ||
|
@@ -166,33 +216,10 @@ aa('setUserToken', 'your-user-token');`); | |
insightsClient, | ||
})({ instantSearchInstance }); | ||
middleware.subscribe(); | ||
expect(getUserToken()).toEqual(ANONYMOUS_TOKEN); | ||
expect(getUserToken()).toEqual('token-from-queue-before-init'); | ||
}); | ||
|
||
describe('umd', () => { | ||
const createUmdTestEnvironment = () => { | ||
const { | ||
insightsClient, | ||
libraryLoadedAndProcessQueue, | ||
} = createInsightsUmdVersion(); | ||
const instantSearchInstance = createInstantSearch({ | ||
client: algoliasearch('myAppId', 'myApiKey'), | ||
}); | ||
const helper = algoliasearchHelper({} as SearchClient, ''); | ||
const getUserToken = () => { | ||
return (helper.state as any).userToken; | ||
}; | ||
instantSearchInstance.mainIndex = { | ||
getHelper: () => helper, | ||
} as Index; | ||
return { | ||
insightsClient, | ||
libraryLoadedAndProcessQueue, | ||
instantSearchInstance, | ||
helper, | ||
getUserToken, | ||
}; | ||
}; | ||
it('applies userToken from queue if exists', () => { | ||
const { | ||
insightsClient, | ||
|
@@ -222,6 +249,7 @@ aa('setUserToken', 'your-user-token');`); | |
insightsClient, | ||
instantSearchInstance, | ||
getUserToken, | ||
libraryLoadedAndProcessQueue, | ||
} = createUmdTestEnvironment(); | ||
|
||
// call init and setUserToken even before the library is loaded. | ||
|
@@ -236,6 +264,7 @@ aa('setUserToken', 'your-user-token');`); | |
insightsClient, | ||
})({ instantSearchInstance }); | ||
middleware.subscribe(); | ||
libraryLoadedAndProcessQueue(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was missing. We need this because the library will eventually be loaded. |
||
expect(getUserToken()).toEqual('token-from-queue'); | ||
}); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,26 +38,35 @@ export const createInsightsMiddleware: CreateInsightsMiddleware = props => { | |
_insightsClient === null ? (noop as InsightsClient) : _insightsClient; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we type it correctly here, we shouldn't have to cast There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mean regarding |
||
|
||
return ({ instantSearchInstance }) => { | ||
insightsClient('_get', '_hasCredentials', (hasCredentials: boolean) => { | ||
if (!hasCredentials) { | ||
const [appId, apiKey] = getAppIdAndApiKey(instantSearchInstance.client); | ||
insightsClient('_get', '_userToken', (userToken: string) => { | ||
warning( | ||
!userToken, | ||
`You set userToken before \`createInsightsMiddleware()\` and it is ignored. | ||
Please set the token after the \`createInsightsMiddleware()\` call. | ||
|
||
createInsightsMiddleware({ /* ... */ }); | ||
|
||
insightsClient('setUserToken', 'your-user-token'); | ||
// or | ||
aa('setUserToken', 'your-user-token'); | ||
` | ||
); | ||
}); | ||
insightsClient('init', { appId, apiKey }); | ||
} | ||
const [appId, apiKey] = getAppIdAndApiKey(instantSearchInstance.client); | ||
let queuedUserToken: string | undefined = undefined; | ||
let userTokenBeforeInit: string | undefined = undefined; | ||
if (Array.isArray((insightsClient as any).queue)) { | ||
// Context: The umd build of search-insights is asynchronously loaded by the snippet. | ||
// | ||
// When user calls `aa('setUserToken', 'my-user-token')` before `search-insights` is loaded, | ||
// ['setUserToken', 'my-user-token'] gets stored in `aa.queue`. | ||
// Whenever `search-insights` is finally loaded, it will process the queue. | ||
// | ||
// But here's the reason why we handle it here: | ||
// At this point, even though `search-insights` is not loaded yet, | ||
// we still want to read the token from the queue. | ||
// Otherwise, the first search call will be fired without the token. | ||
(insightsClient as any).queue.forEach(([method, firstArgument]) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why using find(insightsClient.queue.slice().reverse(), /* ... */)
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
if (method === 'setUserToken') { | ||
queuedUserToken = firstArgument; | ||
} | ||
}); | ||
} | ||
insightsClient('_get', '_userToken', (userToken: string) => { | ||
// If user has called `aa('setUserToken', 'my-user-token')` before creating | ||
// the `insights` middleware, we store them temporarily and | ||
// set it later on. | ||
// | ||
// Otherwise, the `init` call might override it with anonymous user token. | ||
userTokenBeforeInit = userToken; | ||
}); | ||
insightsClient('init', { appId, apiKey }); | ||
|
||
return { | ||
onStateChange() {}, | ||
|
@@ -76,28 +85,19 @@ aa('setUserToken', 'your-user-token'); | |
.getHelper()! | ||
.setQueryParameter('clickAnalytics', true); | ||
|
||
if (hasInsightsClient) { | ||
const anonymousUserToken = getInsightsAnonymousUserTokenInternal(); | ||
if (hasInsightsClient && anonymousUserToken) { | ||
// When `aa('init', { ... })` is called, it creates an anonymous user token in cookie. | ||
// We can set it as userToken. | ||
setUserTokenToSearch(getInsightsAnonymousUserTokenInternal()); | ||
setUserTokenToSearch(anonymousUserToken); | ||
} | ||
|
||
if (queuedUserToken) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why can this condition and the one below be satisfied at the same time? It makes the flow harder to understand. If that what it means, I'd suggest changing to this: // We consider the `userToken` coming from a `init` call to have a higher
// importance than the one coming from the queue.
if (userTokenBeforeInit) {
insightsClient('setUserToken', userTokenBeforeInit);
} else if (queuedUserToken) {
insightsClient('setUserToken', queuedUserToken);
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
insightsClient('setUserToken', queuedUserToken); | ||
} | ||
|
||
if (Array.isArray((insightsClient as any).queue)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This part has been move to the line 44. And there it doesn't call |
||
// Context: The umd build of search-insights is asynchronously loaded by the snippet. | ||
// | ||
// When user calls `aa('setUserToken', 'my-user-token')` before `search-insights` is loaded, | ||
// ['setUserToken', 'my-user-token'] gets stored in `aa.queue`. | ||
// Whenever `search-insights` is finally loaded, it will process the queue. | ||
// | ||
// But here's the reason why we handle it here: | ||
// At this point, even though `search-insights` is not loaded yet, | ||
// we still want to read the token from the queue. | ||
// Otherwise, the first search call will be fired without the token. | ||
(insightsClient as any).queue.forEach(([method, firstArgument]) => { | ||
if (method === 'setUserToken') { | ||
setUserTokenToSearch(firstArgument); | ||
} | ||
}); | ||
if (userTokenBeforeInit) { | ||
insightsClient('setUserToken', userTokenBeforeInit); | ||
} | ||
|
||
// This updates userToken which is set explicitly by `aa('setUserToken', userToken)` | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,13 +26,23 @@ export function createAlgoliaAnalytics() { | |
callback(values._userToken); | ||
} | ||
}; | ||
const sendEvent = () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we implement another whole logic in its mock? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. I thought about it again, and I think the new test is better. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We probably shouldn't even mock any logic, but just spy for methods called. That's coming from before so it shouldn't block that PR though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At some point, I began to wonder if I just should've used the real search-insights client for the tests, because there are some implementations on the search-insights side that needs to be tested with the insights middleware. |
||
if (!values._hasCredentials) { | ||
throw new Error( | ||
"Before calling any methods on the analytics, you first need to call the 'init' function with appId and apiKey parameters" | ||
); | ||
} | ||
}; | ||
const viewedObjectIDs = jest.fn(() => { | ||
sendEvent(); | ||
}); | ||
|
||
return { | ||
setUserToken, | ||
init, | ||
_get, | ||
onUserTokenChange, | ||
viewedObjectIDs: jest.fn(), | ||
viewedObjectIDs, | ||
}; | ||
} | ||
|
||
|
@@ -64,6 +74,9 @@ export function createInsightsUmdVersion() { | |
queue.push = ([methodName, ...args]) => { | ||
_aa(methodName, ...args); | ||
}; | ||
return { | ||
algoliaAnalytics: instance, | ||
}; | ||
}, | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has been moved here from other
describe
scope with no change.