Skip to content
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

feat(useCookie): skip anonymous userToken if useCookie is false #236

Merged
merged 12 commits into from
Dec 22, 2020
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`. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

after this, I think we should release a version with useCookie as true

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean false as a breaking change? It's true by default.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, sorry I meant false in the future

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah it should be false in the next major version.

| `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,31 +33,31 @@ 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)
analyticsInstance.setAnonymousUserToken()
).toThrowErrorMatchingInlineSnapshot(
`"Tracking of anonymous users is only possible on environments which support cookies."`
);
Expand All @@ -72,11 +72,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
61 changes: 49 additions & 12 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,31 +119,54 @@ 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);
const setUserToken = jest.spyOn(analyticsInstance, "setUserToken");
const setAnonymousUserToken = jest.spyOn(
analyticsInstance,
"setAnonymousUserToken"
);

analyticsInstance.init({ apiKey: "***", appId: "XXX", region: "de" });
expect(setUserToken).not.toHaveBeenCalled();
expect(setAnonymousUserToken).not.toHaveBeenCalled();

setAnonymousUserToken.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();

setUserToken.mockRestore();
setAnonymousUserToken.mockRestore();
supportsCookies.mockRestore();
});

Expand Down Expand Up @@ -199,6 +222,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
40 changes: 19 additions & 21 deletions lib/_cookieUtils.ts → lib/_tokenUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,29 +25,27 @@ 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()) {
Haroenv marked this conversation as resolved.
Show resolved Hide resolved
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.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
7 changes: 4 additions & 3 deletions lib/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
samouss marked this conversation as resolved.
Show resolved Hide resolved
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 && supportsCookies()) {
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