Skip to content

Commit

Permalink
feat: add support for authenticatedUserToken (#513)
Browse files Browse the repository at this point in the history
### What
* Add `authenticatedUserToken` field to event types
* Add `setAuthenticatedUserToken` method to enable users to set the
field to be automatically passed in subsequent `sendEvent` calls
* Add `getAuthenticatedUserToken` method to enable users to get the
`authenticatedUserToken` field
* Add `onAuthenticatedUserTokenChange` to enable passing a callback on
`authenticatedUserToken` change
* Enable `authenticatedUserToken` to be passed on `init`

### Links
[EEX-746](https://algolia.atlassian.net/browse/EEX-746)

More context:
* [Insights user identity proposal
(Confluence)](https://algolia.atlassian.net/wiki/spaces/PREDICT/pages/4559470786/Insights+user+identity+proposal)
* [Authenticated user token in Insights pipeline (Confluence)](
https://algolia.atlassian.net/wiki/spaces/EX/pages/4576346340/2023-05-25+Authenticated+user+token+in+Insights+pipeline)
  • Loading branch information
jkaho authored Oct 13, 2023
1 parent 7ea0c01 commit 0636a2d
Show file tree
Hide file tree
Showing 12 changed files with 328 additions and 5 deletions.
12 changes: 10 additions & 2 deletions docs/nodejs.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,19 @@ aa("init", {

## 3. Add `userToken`

On the Node.js environment, unlike the browser environment, `userToken` must be specified when sending any event.
On the Node.js environment, unlike the browser environment, a [user token](https://www.algolia.com/doc/guides/sending-events/concepts/usertoken) (required `userToken` and optional `authenticatedUserToken`) must be specified when sending any event.

```js
// Anonymous user ID
aa("clickedObjectIDs", {
userToken: "USER_ID"
userToken: "ANONYMOUS_ID"
// ...
});

// Authenticated user ID
aa("clickedObjectIDs", {
userToken: "ANONYMOUS_ID",
authenticatedUserToken: "USER_ID"
// ...
});
```
Expand Down
111 changes: 111 additions & 0 deletions lib/__tests__/_sendEvent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,41 @@ describe("sendEvents", () => {
});
});

it("should be added by default even if authenticatedUserToken is provided", () => {
analyticsInstance.setAuthenticatedUserToken("my-user-token");

analyticsInstance.sendEvents(
[
{
eventType: "click",
eventName: "my-event",
index: "my-index",
objectIDs: ["1"]
}
],
{
headers: {
"X-Algolia-Application-Id": "algoliaAppId",
"X-Algolia-API-Key": "algoliaApiKey"
}
}
);
expect(XMLHttpRequest.send).toHaveBeenCalledTimes(1);
const payload = JSON.parse(XMLHttpRequest.send.mock.calls[0][0]);
expect(payload).toEqual({
events: [
{
eventType: "click",
eventName: "my-event",
index: "my-index",
objectIDs: ["1"],
authenticatedUserToken: "my-user-token",
userToken: expect.stringMatching(/^anonymous-/)
}
]
});
});

it("should not be added if anonymousUserToken: false", () => {
analyticsInstance.init({ anonymousUserToken: false });
expect(analyticsInstance._anonymousUserToken).toBe(false);
Expand Down Expand Up @@ -495,6 +530,82 @@ describe("sendEvents", () => {
});
});

describe("authenticatedUserToken", () => {
let analyticsInstance: AlgoliaAnalytics;
beforeEach(() => {
analyticsInstance = setupInstance();
});

it("should add authenticatedUserToken if initially set and not provided", () => {
analyticsInstance.setAuthenticatedUserToken("authed-user-id");
analyticsInstance.sendEvents([
{
eventType: "click",
eventName: "my-event",
index: "my-index",
objectIDs: ["1"]
}
]);
expect(XMLHttpRequest.send).toHaveBeenCalledTimes(1);
const payload = JSON.parse(XMLHttpRequest.send.mock.calls[0][0]);
expect(payload).toEqual({
events: [
expect.objectContaining({
authenticatedUserToken: "authed-user-id"
})
]
});
});

it("should not add authenticatedUserToken if not initially set and not provided", () => {
analyticsInstance.sendEvents([
{
eventType: "click",
eventName: "my-event",
index: "my-index",
objectIDs: ["1"]
}
]);
expect(XMLHttpRequest.send).toHaveBeenCalledTimes(1);
const payload = JSON.parse(XMLHttpRequest.send.mock.calls[0][0]);
expect(payload).toEqual({
events: [
{
eventType: "click",
eventName: "my-event",
index: "my-index",
objectIDs: ["1"],
userToken: "mock-user-id"
}
]
});
});

it("should pass over provided authenticatedUserToken", () => {
analyticsInstance.setAuthenticatedUserToken("authed-user-id");
analyticsInstance.sendEvents([
{
eventType: "click",
eventName: "my-event",
index: "my-index",
objectIDs: ["1"],
userToken: "007",
authenticatedUserToken: "008"
}
]);
expect(XMLHttpRequest.send).toHaveBeenCalledTimes(1);
const payload = JSON.parse(XMLHttpRequest.send.mock.calls[0][0]);
expect(payload).toEqual({
events: [
expect.objectContaining({
userToken: "007",
authenticatedUserToken: "008"
})
]
});
});
});

describe("filters", () => {
let analyticsInstance: AlgoliaAnalytics;
beforeEach(() => {
Expand Down
42 changes: 42 additions & 0 deletions lib/__tests__/_tokenUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ describe("tokenUtils", () => {
// clear cookies
document.cookie = "_ALGOLIA=;expires=Thu, 01-Jan-1970 00:00:01 GMT;";
});

describe("setUserToken", () => {
describe("anonymous userToken", () => {
it("should create a cookie with a UUID", () => {
Expand All @@ -52,6 +53,7 @@ describe("tokenUtils", () => {
expect(document.cookie).toBe("_ALGOLIA=anonymous-mock-uuid-2");
});
});

describe("provided userToken", () => {
it("should not create a cookie with provided userToken", () => {
analyticsInstance.setUserToken("007");
Expand All @@ -71,6 +73,20 @@ describe("tokenUtils", () => {
});
});
});

describe("setAuthenticatedUserToken", () => {
it("should set authenticatedUserToken", () => {
expect(analyticsInstance._authenticatedUserToken).toBeUndefined();

analyticsInstance.setAuthenticatedUserToken("008");
expect(analyticsInstance._authenticatedUserToken).toBe("008");
});
it("should not create a cookie with provided authenticatedUserToken", () => {
analyticsInstance.setAuthenticatedUserToken("008");
expect(document.cookie).toBe("");
});
});

describe("getUserToken", () => {
beforeEach(() => {
analyticsInstance.setUserToken("007");
Expand All @@ -80,6 +96,7 @@ describe("tokenUtils", () => {
expect(userToken).toEqual("007");
});
it("should accept a callback", () => {
expect.assertions(2);
analyticsInstance.getUserToken({}, (err, userToken) => {
expect(err).toEqual(null);
expect(userToken).toEqual("007");
Expand All @@ -104,4 +121,29 @@ describe("tokenUtils", () => {
});
});
});

describe("getAuthenticatedUserToken", () => {
it("should return undefined if not set", () => {
const authenticatedUserToken =
analyticsInstance.getAuthenticatedUserToken();
expect(authenticatedUserToken).toBeUndefined();
});
it("should return current authenticatedUserToken", () => {
analyticsInstance.setAuthenticatedUserToken("008");
const authenticatedUserToken =
analyticsInstance.getAuthenticatedUserToken();
expect(authenticatedUserToken).toEqual("008");
});
it("should accept a callback", () => {
expect.assertions(2);
analyticsInstance.setAuthenticatedUserToken("009");
analyticsInstance.getAuthenticatedUserToken(
{},
(err, authenticatedUserToken) => {
expect(err).toEqual(null);
expect(authenticatedUserToken).toEqual("009");
}
);
});
});
});
87 changes: 87 additions & 0 deletions lib/__tests__/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,33 @@ describe("init", () => {
setAnonymousUserToken.mockRestore();
supportsCookies.mockRestore();
});
it("should set anonymous userToken even if authenticatedUserToken is set", () => {
const setAuthenticatedUserToken = jest.spyOn(
analyticsInstance,
"setAuthenticatedUserToken"
);
const setUserToken = jest.spyOn(analyticsInstance, "setUserToken");
analyticsInstance.init({
apiKey: "***",
appId: "XXX",
useCookie: true,
authenticatedUserToken: "abc"
});
expect(setAuthenticatedUserToken).toHaveBeenCalledTimes(1);
expect(setAuthenticatedUserToken).toHaveBeenCalledWith("abc");
expect(setUserToken).toHaveBeenCalledTimes(1);
expect(setUserToken).toHaveBeenCalledWith(
expect.stringMatching(/^anonymous-/)
);

expect(analyticsInstance._userToken).toEqual(
expect.stringMatching(/^anonymous-/)
);
expect(analyticsInstance._authenticatedUserToken).toBe("abc");

setAuthenticatedUserToken.mockRestore();
setUserToken.mockRestore();
});
it("should not set anonymous userToken if environment does not supports cookies", () => {
const supportsCookies = jest
.spyOn(utils, "supportsCookies")
Expand Down Expand Up @@ -487,6 +514,7 @@ describe("init", () => {
});

it("can set userToken manually afterwards", (done) => {
expect.assertions(3);
analyticsInstance.init({ apiKey: "***", appId: "XXX", userToken: "abc" });
analyticsInstance.setUserToken("def");
expect(setUserToken).toHaveBeenCalledTimes(2);
Expand All @@ -497,4 +525,63 @@ describe("init", () => {
});
});
});

describe("authenticatedUserToken param", () => {
let setAuthenticatedUserToken: jest.SpyInstance<
number | string,
[authenticatedUserToken: number | string]
>;
beforeEach(() => {
setAuthenticatedUserToken = jest.spyOn(
analyticsInstance,
"setAuthenticatedUserToken"
);
});

afterEach(() => {
setAuthenticatedUserToken.mockRestore();
});

it("should set authenticatedUserToken", () => {
expect.assertions(3);
analyticsInstance.init({
apiKey: "***",
appId: "XXX",
authenticatedUserToken: "abc"
});
expect(setAuthenticatedUserToken).toHaveBeenCalledTimes(1);
expect(setAuthenticatedUserToken).toHaveBeenCalledWith("abc");
analyticsInstance.getAuthenticatedUserToken(null, (_err, value) => {
expect(value).toEqual("abc");
});
});

it("can set authenticatedUserToken manually afterwards", (done) => {
expect.assertions(3);
analyticsInstance.init({
apiKey: "***",
appId: "XXX",
authenticatedUserToken: "abc"
});
analyticsInstance.setAuthenticatedUserToken("def");
expect(setAuthenticatedUserToken).toHaveBeenCalledTimes(2);
expect(setAuthenticatedUserToken).toHaveBeenLastCalledWith("def");
analyticsInstance.getAuthenticatedUserToken(null, (_err, value) => {
expect(value).toEqual("def");
done();
});
});

it("should not set authenticatedUserToken if not passed", () => {
expect.assertions(2);
analyticsInstance.init({
apiKey: "***",
appId: "XXX"
});
expect(setAuthenticatedUserToken).toHaveBeenCalledTimes(0);
analyticsInstance.getAuthenticatedUserToken(null, (_err, value) => {
expect(value).toBeUndefined();
});
});
});
});
4 changes: 3 additions & 1 deletion lib/_sendEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ export function makeSendEvents(requestFn: RequestFnType) {

const payload: InsightsEvent = {
...rest,
userToken: data?.userToken ?? this._userToken
userToken: data?.userToken ?? this._userToken,
authenticatedUserToken:
data?.authenticatedUserToken ?? this._authenticatedUserToken
};
if (!isUndefined(filters)) {
payload.filters = filters.map(encodeURIComponent);
Expand Down
37 changes: 37 additions & 0 deletions lib/_tokenUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,40 @@ export function onUserTokenChange(
this._onUserTokenChangeCallback(this._userToken);
}
}

export function setAuthenticatedUserToken(
this: AlgoliaAnalytics,
authenticatedUserToken: number | string
): number | string {
this._authenticatedUserToken = authenticatedUserToken;
if (isFunction(this._onAuthenticatedUserTokenChangeCallback)) {
this._onAuthenticatedUserTokenChangeCallback(this._authenticatedUserToken);
}
return this._authenticatedUserToken;
}

export function getAuthenticatedUserToken(
this: AlgoliaAnalytics,
options?: any,
callback?: (err: any, authenticatedUserToken?: number | string) => void
): number | string | undefined {
if (isFunction(callback)) {
callback(null, this._authenticatedUserToken);
}
return this._authenticatedUserToken;
}

export function onAuthenticatedUserTokenChange(
this: AlgoliaAnalytics,
callback?: (authenticatedUserToken?: number | string) => void,
options?: { immediate: boolean }
): void {
this._onAuthenticatedUserTokenChangeCallback = callback;
if (
options &&
options.immediate &&
isFunction(this._onAuthenticatedUserTokenChangeCallback)
) {
this._onAuthenticatedUserTokenChangeCallback(this._authenticatedUserToken);
}
}
Loading

0 comments on commit 0636a2d

Please sign in to comment.