Skip to content

Commit

Permalink
feat(useCookie): skip anonymous userToken if useCookie is false (#236)
Browse files Browse the repository at this point in the history
* rename cookieUtils to tokenUtils

* add useCookie option

* extract as setAnonymousUserToken

* skip anonymous userToken if useCookie is false

* update readme

* fix lint error

* call onUserTokenChangeCallback when setting anonymous user token

* do not throw when setting anonymous user token in no-cookie environment

* add more description to useCookie

* remove unused import
  • Loading branch information
Eunjae Lee authored Dec 22, 2020
1 parent dd46015 commit db09c7e
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 60 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/__tests__/_sendEvent.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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({
Expand All @@ -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", () => {
Expand All @@ -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");
Expand Down
52 changes: 43 additions & 9 deletions lib/__tests__/init.test.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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", () => {
Expand Down Expand Up @@ -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", () => {
Expand Down
38 changes: 17 additions & 21 deletions lib/_cookieUtils.ts → lib/_tokenUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
9 changes: 5 additions & 4 deletions lib/init.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -9,6 +9,7 @@ export interface InitParams {
apiKey: string;
appId: string;
userHasOptedOut?: boolean;
useCookie?: boolean;
cookieDuration?: number;
region?: InsightRegion;
}
Expand Down Expand Up @@ -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;
Expand All @@ -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();
}
}
9 changes: 5 additions & 4 deletions lib/insights.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -69,6 +69,7 @@ class AlgoliaAnalytics {
_endpointOrigin: string;
_userToken: string;
_userHasOptedOut: boolean;
_useCookie: boolean;
_cookieDuration: number;

// user agent
Expand All @@ -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
Expand Down Expand Up @@ -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);

Expand Down

0 comments on commit db09c7e

Please sign in to comment.