diff --git a/README.md b/README.md index dca516b3..f619f1bf 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ aa('setUserToken', 'USER_ID'); | **`apiKey`** | `string` | None (required) | The search API key of your Algolia application | | `userHasOptedOut` | `boolean` | `false` | Whether to exclude users from analytics | | `region` | `'de' \| 'us'` | Automatic | The DNS server to target | +| `useCookie` | `boolean` | `true` | Whether to use cookie in browser environment. The anonymous user token will not be set if `false`. When `useCookie` is `false` and `setUserToken` is not called yet, sending events will throw errors because there is no user token to attach to the events. | | `cookieDuration` | `number` | `15552000000` (6 months) | The cookie duration in milliseconds | ### Node.js diff --git a/lib/__tests__/_sendEvent.node.test.ts b/lib/__tests__/_sendEvent.node.test.ts index 14b987f4..95ba2058 100644 --- a/lib/__tests__/_sendEvent.node.test.ts +++ b/lib/__tests__/_sendEvent.node.test.ts @@ -4,7 +4,7 @@ import AlgoliaAnalytics from "../insights"; import { getRequesterForNode } from "../utils/getRequesterForNode"; import { getFunctionalInterface } from "../_getFunctionalInterface"; -import { setUserToken } from "../_cookieUtils"; +import { setUserToken } from "../_tokenUtils"; const credentials = { apiKey: "testKey", diff --git a/lib/__tests__/_cookieUtils.test.ts b/lib/__tests__/_tokenUtils.test.ts similarity index 75% rename from lib/__tests__/_cookieUtils.test.ts rename to lib/__tests__/_tokenUtils.test.ts index 1c7a86c2..689c7164 100644 --- a/lib/__tests__/_cookieUtils.test.ts +++ b/lib/__tests__/_tokenUtils.test.ts @@ -1,4 +1,4 @@ -import { getCookie } from "../_cookieUtils"; +import { getCookie } from "../_tokenUtils"; import AlgoliaAnalytics from "../insights"; import { createUUID } from "../utils/uuid"; import * as utils from "../utils"; @@ -17,7 +17,7 @@ const DAY = 86400000; /* 1 day in ms*/ const DATE_TOMORROW = new Date(Date.now() + DAY).toUTCString(); const DATE_YESTERDAY = new Date(Date.now() - DAY).toUTCString(); -describe("cookieUtils", () => { +describe("tokenUtils", () => { let analyticsInstance; beforeEach(() => { analyticsInstance = new AlgoliaAnalytics({ @@ -33,36 +33,25 @@ describe("cookieUtils", () => { document.cookie = "_ALGOLIA=;expires=Thu, 01-Jan-1970 00:00:01 GMT;"; }); describe("setUserToken", () => { - describe("ANONYMOUS_USER_TOKEN", () => { + describe("anonymous userToken", () => { it("should create a cookie with a UUID", () => { - analyticsInstance.setUserToken(analyticsInstance.ANONYMOUS_USER_TOKEN); + analyticsInstance.setAnonymousUserToken(); expect(document.cookie).toBe("_ALGOLIA=anonymous-mock-uuid-1"); }); it("should reuse previously created UUID", () => { - analyticsInstance.setUserToken(analyticsInstance.ANONYMOUS_USER_TOKEN); + analyticsInstance.setAnonymousUserToken(); expect(document.cookie).toBe("_ALGOLIA=anonymous-mock-uuid-1"); - analyticsInstance.setUserToken(analyticsInstance.ANONYMOUS_USER_TOKEN); + analyticsInstance.setAnonymousUserToken(); expect(document.cookie).toBe("_ALGOLIA=anonymous-mock-uuid-1"); }); it("should not reuse UUID from an expired cookie", () => { - analyticsInstance.setUserToken(analyticsInstance.ANONYMOUS_USER_TOKEN); + analyticsInstance.setAnonymousUserToken(); expect(document.cookie).toBe("_ALGOLIA=anonymous-mock-uuid-1"); // set cookie as expired document.cookie = "_ALGOLIA=;expires=Thu, 01-Jan-1970 00:00:01 GMT;"; - analyticsInstance.setUserToken(analyticsInstance.ANONYMOUS_USER_TOKEN); + analyticsInstance.setAnonymousUserToken(); expect(document.cookie).toBe("_ALGOLIA=anonymous-mock-uuid-2"); }); - it("should throw if environment does not support cookies", () => { - const mockSupportsCookies = jest - .spyOn(utils, "supportsCookies") - .mockReturnValue(false); - expect(() => - analyticsInstance.setUserToken(analyticsInstance.ANONYMOUS_USER_TOKEN) - ).toThrowErrorMatchingInlineSnapshot( - `"Tracking of anonymous users is only possible on environments which support cookies."` - ); - mockSupportsCookies.mockRestore(); - }); }); describe("provided userToken", () => { it("should not create a cookie with provided userToken", () => { @@ -72,11 +61,11 @@ describe("cookieUtils", () => { it("create a anonymous cookie when switching from provided userToken to anonymous", () => { analyticsInstance.setUserToken("007"); expect(document.cookie).toBe(""); - analyticsInstance.setUserToken(analyticsInstance.ANONYMOUS_USER_TOKEN); + analyticsInstance.setAnonymousUserToken(); expect(document.cookie).toBe("_ALGOLIA=anonymous-mock-uuid-1"); }); it("should preserve the cookie with same uuid when userToken provided after anonymous", () => { - analyticsInstance.setUserToken(analyticsInstance.ANONYMOUS_USER_TOKEN); + analyticsInstance.setAnonymousUserToken(); expect(document.cookie).toBe("_ALGOLIA=anonymous-mock-uuid-1"); analyticsInstance.setUserToken("007"); expect(document.cookie).toBe("_ALGOLIA=anonymous-mock-uuid-1"); diff --git a/lib/__tests__/init.test.ts b/lib/__tests__/init.test.ts index 23cd4c80..0a50e865 100644 --- a/lib/__tests__/init.test.ts +++ b/lib/__tests__/init.test.ts @@ -1,6 +1,6 @@ import AlgoliaAnalytics from "../insights"; import * as utils from "../utils"; -import { getCookie } from "../_cookieUtils"; +import { getCookie } from "../_tokenUtils"; describe("init", () => { let analyticsInstance; @@ -119,22 +119,22 @@ describe("init", () => { "https://insights.de.algolia.io" ); }); - it("should set userToken to ANONYMOUS if environment supports cookies", () => { + it("should set anonymous userToken if environment supports cookies", () => { const supportsCookies = jest .spyOn(utils, "supportsCookies") .mockReturnValue(true); - const setUserToken = jest.spyOn(analyticsInstance, "setUserToken"); + const setAnonymousUserToken = jest.spyOn( + analyticsInstance, + "setAnonymousUserToken" + ); analyticsInstance.init({ apiKey: "***", appId: "XXX", region: "de" }); - expect(setUserToken).toHaveBeenCalledWith( - analyticsInstance.ANONYMOUS_USER_TOKEN - ); - expect(setUserToken).toHaveBeenCalledTimes(1); + expect(setAnonymousUserToken).toHaveBeenCalledTimes(1); - setUserToken.mockRestore(); + setAnonymousUserToken.mockRestore(); supportsCookies.mockRestore(); }); - it("should not set userToken if environment does not supports cookies", () => { + it("should not set anonymous userToken if environment does not supports cookies", () => { const supportsCookies = jest .spyOn(utils, "supportsCookies") .mockReturnValue(false); @@ -146,6 +146,26 @@ describe("init", () => { setUserToken.mockRestore(); supportsCookies.mockRestore(); }); + it("should not set anonymous userToken if useCookie is false", () => { + const supportsCookies = jest + .spyOn(utils, "supportsCookies") + .mockReturnValue(true); + const setAnonymousUserToken = jest.spyOn( + analyticsInstance, + "setAnonymousUserToken" + ); + + analyticsInstance.init({ + apiKey: "***", + appId: "XXX", + region: "de", + useCookie: false + }); + expect(setAnonymousUserToken).not.toHaveBeenCalled(); + + setAnonymousUserToken.mockRestore(); + supportsCookies.mockRestore(); + }); describe("callback for userToken", () => { describe("immediate: true", () => { @@ -199,6 +219,20 @@ describe("init", () => { expect(callback).toHaveBeenCalledWith("def"); expect(callback).toHaveBeenCalledTimes(1); }); + + it("is triggered by setAnonymousUserToken", () => { + analyticsInstance.init({ apiKey: "***", appId: "XXX", region: "de" }); + + const callback = jest.fn(); + analyticsInstance.onUserTokenChange(callback); + expect(callback).toHaveBeenCalledTimes(0); + + analyticsInstance.setAnonymousUserToken(); + expect(callback).toHaveBeenCalledWith( + expect.stringMatching(/^anonymous-[-\w]+$/) + ); + expect(callback).toHaveBeenCalledTimes(1); + }); }); describe("nullish or invalid callback", () => { diff --git a/lib/_cookieUtils.ts b/lib/_tokenUtils.ts similarity index 70% rename from lib/_cookieUtils.ts rename to lib/_tokenUtils.ts index da931d22..3653e398 100644 --- a/lib/_cookieUtils.ts +++ b/lib/_tokenUtils.ts @@ -25,29 +25,25 @@ export const getCookie = (name: string): string => { return ""; }; -export const ANONYMOUS_USER_TOKEN = "ANONYMOUS_USER_TOKEN"; - -export function setUserToken(userToken: string | number): void { - if (userToken === ANONYMOUS_USER_TOKEN) { - if (!supportsCookies()) { - throw new Error( - "Tracking of anonymous users is only possible on environments which support cookies." - ); - } - const foundToken = getCookie(COOKIE_KEY); - if ( - !foundToken || - foundToken === "" || - foundToken.indexOf("anonymous-") !== 0 - ) { - this._userToken = `anonymous-${createUUID()}`; - setCookie(COOKIE_KEY, this._userToken, this._cookieDuration); - } else { - this._userToken = foundToken; - } +export function setAnonymousUserToken(): void { + if (!supportsCookies()) { + return; + } + const foundToken = getCookie(COOKIE_KEY); + if ( + !foundToken || + foundToken === "" || + foundToken.indexOf("anonymous-") !== 0 + ) { + this.setUserToken(`anonymous-${createUUID()}`); + setCookie(COOKIE_KEY, this._userToken, this._cookieDuration); } else { - this._userToken = userToken; + this.setUserToken(foundToken); } +} + +export function setUserToken(userToken: string | number): void { + this._userToken = userToken; if (isFunction(this._onUserTokenChangeCallback)) { this._onUserTokenChangeCallback(this._userToken); } diff --git a/lib/init.ts b/lib/init.ts index 5b93ce0e..406dcc81 100644 --- a/lib/init.ts +++ b/lib/init.ts @@ -1,4 +1,4 @@ -import { isUndefined, isString, isNumber, supportsCookies } from "./utils"; +import { isUndefined, isString, isNumber } from "./utils"; import { DEFAULT_ALGOLIA_AGENT } from "./_algoliaAgent"; type InsightRegion = "de" | "us"; @@ -9,6 +9,7 @@ export interface InitParams { apiKey: string; appId: string; userHasOptedOut?: boolean; + useCookie?: boolean; cookieDuration?: number; region?: InsightRegion; } @@ -61,7 +62,7 @@ export function init(options: InitParams) { this._endpointOrigin = options.region ? `https://insights.${options.region}.algolia.io` : "https://insights.algolia.io"; - + this._useCookie = options.useCookie ?? true; this._cookieDuration = options.cookieDuration ? options.cookieDuration : 6 * MONTH; @@ -72,7 +73,7 @@ export function init(options: InitParams) { this._ua = DEFAULT_ALGOLIA_AGENT; this._uaURIEncoded = encodeURIComponent(DEFAULT_ALGOLIA_AGENT); - if (!this._userHasOptedOut && supportsCookies()) { - this.setUserToken(this.ANONYMOUS_USER_TOKEN); + if (!this._userHasOptedOut && this._useCookie) { + this.setAnonymousUserToken(); } } diff --git a/lib/insights.ts b/lib/insights.ts index b700a62b..c61a4dc7 100644 --- a/lib/insights.ts +++ b/lib/insights.ts @@ -36,11 +36,11 @@ import { viewedFilters } from "./view"; import { - ANONYMOUS_USER_TOKEN, getUserToken, setUserToken, + setAnonymousUserToken, onUserTokenChange -} from "./_cookieUtils"; +} from "./_tokenUtils"; import { version } from "../package.json"; type Queue = { @@ -69,6 +69,7 @@ class AlgoliaAnalytics { _endpointOrigin: string; _userToken: string; _userHasOptedOut: boolean; + _useCookie: boolean; _cookieDuration: number; // user agent @@ -89,8 +90,8 @@ class AlgoliaAnalytics { public addAlgoliaAgent: (algoliaAgent: string) => void; - public ANONYMOUS_USER_TOKEN: string; public setUserToken: (userToken: string) => void; + public setAnonymousUserToken: () => void; public getUserToken: ( options?: any, callback?: (err: any, userToken: string) => void @@ -130,8 +131,8 @@ class AlgoliaAnalytics { this.addAlgoliaAgent = addAlgoliaAgent.bind(this); - this.ANONYMOUS_USER_TOKEN = ANONYMOUS_USER_TOKEN; this.setUserToken = setUserToken.bind(this); + this.setAnonymousUserToken = setAnonymousUserToken.bind(this); this.getUserToken = getUserToken.bind(this); this.onUserTokenChange = onUserTokenChange.bind(this);