Skip to content

Commit

Permalink
fix: restore user token which was set before creating the middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
eunjae-lee committed Nov 9, 2020
1 parent 97244a5 commit 9effefd
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 97 deletions.
102 changes: 51 additions & 51 deletions src/middlewares/__tests__/createInsightsMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
});
Expand Down Expand Up @@ -79,40 +103,37 @@ describe('insights', () => {
});
});

it('does not throw when an event is sent right after the creation', () => {
const { insightsClient, instantSearchInstance } = createTestEnvironment();
createInsightsMiddleware({
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();

setTimeout(() => {
libraryLoadedAndProcessQueue();
done();
}, 20);

expect(() => {
insightsClient('viewedObjectIDs', {
userToken: 'my-user-token',
index: instantSearchInstance.indexName,
eventName: 'Products Viewed',
objectIDs: ['obj-id0', 'obj-id1'],
instantSearchInstance.sendEventToInsights({
eventType: 'view',
insightsMethod: 'viewedObjectIDs',
payload: {
eventName: 'Hits Viewed',
index: '',
objectIDs: ['1', '2'],
},
widgetType: 'ais.hits',
});
}).not.toThrow();
});

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.
createInsightsMiddleware({ /* ... */ });
insightsClient('setUserToken', 'your-user-token');
// or
aa('setUserToken', 'your-user-token');`);
});

it('applies clickAnalytics', () => {
const {
insightsClient,
Expand Down Expand Up @@ -169,7 +190,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,
Expand All @@ -182,33 +203,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,
Expand Down Expand Up @@ -238,6 +236,7 @@ aa('setUserToken', 'your-user-token');`);
insightsClient,
instantSearchInstance,
getUserToken,
libraryLoadedAndProcessQueue,
} = createUmdTestEnvironment();

// call init and setUserToken even before the library is loaded.
Expand All @@ -252,6 +251,7 @@ aa('setUserToken', 'your-user-token');`);
insightsClient,
})({ instantSearchInstance });
middleware.subscribe();
libraryLoadedAndProcessQueue();
expect(getUserToken()).toEqual('token-from-queue');
});

Expand Down
74 changes: 37 additions & 37 deletions src/middlewares/createInsightsMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,26 +38,35 @@ export const createInsightsMiddleware: CreateInsightsMiddleware = props => {
_insightsClient === null ? (noop as InsightsClient) : _insightsClient;

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]) => {
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() {},
Expand All @@ -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) {
insightsClient('setUserToken', queuedUserToken);
}

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]) => {
if (method === 'setUserToken') {
setUserTokenToSearch(firstArgument);
}
});
if (userTokenBeforeInit) {
insightsClient('setUserToken', userTokenBeforeInit);
}

// This updates userToken which is set explicitly by `aa('setUserToken', userToken)`
Expand Down
8 changes: 7 additions & 1 deletion src/types/insights.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ export type InsightsClientPayload = {
positions?: number[];
};

export type InsightsSetUserToken = (
method: 'setUserToken',
userToken: string
) => void;

export type InsightsSendEvent = (
method: InsightsClientMethod,
payload: InsightsClientPayload
Expand Down Expand Up @@ -40,7 +45,8 @@ export type InsightsInit = (
export type InsightsClient = InsightsSendEvent &
InsightsOnUserTokenChange &
InsightsGet &
InsightsInit;
InsightsInit &
InsightsSetUserToken;

export type InsightsClientWrapper = (
method: InsightsClientMethod,
Expand Down
14 changes: 6 additions & 8 deletions test/mock/createInsightsClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,15 @@ export function createAlgoliaAnalytics() {
callback(values._userToken);
}
};
const viewedObjectIDs = jest.fn(() => {
if (!values._apiKey) {
throw new Error(
'apiKey is missing, please provide it so we can authenticate the application'
);
}
if (!values._appId) {
const sendEvent = () => {
if (!values._hasCredentials) {
throw new Error(
'appId is missing, please provide it, so we can properly attribute data to your application'
"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 {
Expand Down

0 comments on commit 9effefd

Please sign in to comment.