Skip to content

Commit

Permalink
feat(userToken): accepting provided userToken (#64)
Browse files Browse the repository at this point in the history
* feat(userToken): accepting provided userToken

* fix(eventName): remove unecessary eventName for Search functions

* fix(userToken): set userToken as optional

* fix(uuid): prettier
  • Loading branch information
tkrugg authored Dec 17, 2018
1 parent 55705d3 commit f7d63ee
Show file tree
Hide file tree
Showing 15 changed files with 153 additions and 93 deletions.
69 changes: 59 additions & 10 deletions lib/__tests__/_cookieUtils.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,65 @@
import { userToken } from "../_cookieUtils";
jest.mock("../utils/index", () => ({
createUUID: jest.fn(() => "mock-uuid")
import AlgoliaInsights from "../insights";
import { createUUID } from "../utils/uuid";

jest.mock("../utils/uuid", () => ({
createUUID: jest.fn()
}));

const credentials = {
apiKey: "test",
applicationID: "test",
cookieDuration: 10 * 24 * 3600 * 1000 // 10 days
};

describe("cookieUtils", () => {
// FIXME document.cookie should be fully mocked
// getCookie and setCookie should be put in utils and mocked in this test
describe("userToken", () => {
it("should create a cookie with a UUID", () => {
delete document.cookie;
userToken(null, 1000);
expect(document.cookie).toBe("_ALGOLIA=mock-uuid");
beforeEach(() => {
AlgoliaInsights.init(credentials);
createUUID.mockReset();
createUUID
.mockReturnValueOnce("mock-uuid-1")
.mockReturnValueOnce("mock-uuid-2")
.mockReturnValue("mock-uuid-2+");
// clear cookies
document.cookie = "_ALGOLIA=;expires=Thu, 01-Jan-1970 00:00:01 GMT;";
});
describe("setUserToken", () => {
describe("ANONYMOUS_USER_TOKEN", () => {
it("should create a cookie with a UUID", () => {
AlgoliaInsights.setUserToken(AlgoliaInsights.ANONYMOUS_USER_TOKEN);
expect(document.cookie).toBe("_ALGOLIA=anonymous-mock-uuid-1");
});
it("should reuse previously created UUID", () => {
AlgoliaInsights.setUserToken(AlgoliaInsights.ANONYMOUS_USER_TOKEN);
expect(document.cookie).toBe("_ALGOLIA=anonymous-mock-uuid-1");
AlgoliaInsights.setUserToken(AlgoliaInsights.ANONYMOUS_USER_TOKEN);
expect(document.cookie).toBe("_ALGOLIA=anonymous-mock-uuid-1");
});
it("should not reuse UUID from an expired cookie", () => {
AlgoliaInsights.setUserToken(AlgoliaInsights.ANONYMOUS_USER_TOKEN);
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;";
AlgoliaInsights.setUserToken(AlgoliaInsights.ANONYMOUS_USER_TOKEN);
expect(document.cookie).toBe("_ALGOLIA=anonymous-mock-uuid-2");
});
});
describe("provided userToken", () => {
it("should not create a cookie with provided userToken", () => {
AlgoliaInsights.setUserToken("007");
expect(document.cookie).toBe("");
});
it("create a anonymous cookie when switching from provided userToken to anonymous", () => {
AlgoliaInsights.setUserToken("007");
expect(document.cookie).toBe("");
AlgoliaInsights.setUserToken(AlgoliaInsights.ANONYMOUS_USER_TOKEN);
expect(document.cookie).toBe("_ALGOLIA=anonymous-mock-uuid-1");
});
it("should preserve the cookie with same uuid when userToken provided after anonymous", () => {
AlgoliaInsights.setUserToken(AlgoliaInsights.ANONYMOUS_USER_TOKEN);
expect(document.cookie).toBe("_ALGOLIA=anonymous-mock-uuid-1");
AlgoliaInsights.setUserToken("007");
expect(document.cookie).toBe("_ALGOLIA=anonymous-mock-uuid-1");
});
});
});
});
19 changes: 8 additions & 11 deletions lib/__tests__/_sendEvent.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import AlgoliaInsights from "../insights";
import * as url from "url";

jest.mock("../_cookieUtils", () => ({
userToken: jest.fn(() => "mock-user-id")
}));

const credentials = {
apiKey: "test",
applicationID: "test"
Expand All @@ -15,6 +11,7 @@ describe("sendEvent", () => {

beforeEach(() => {
AlgoliaInsights.init(credentials);
AlgoliaInsights.setUserToken("mock-user-id");
XMLHttpRequest = {
open: jest.spyOn((window as any).XMLHttpRequest.prototype, "open"),
send: jest.spyOn((window as any).XMLHttpRequest.prototype, "send")
Expand Down Expand Up @@ -126,23 +123,23 @@ describe("sendEvent", () => {
});

describe("eventName", () => {
it("should throw if no eventName passed", () => {
it("should not throw if no eventName passed", () => {
expect(() => {
(AlgoliaInsights as any).sendEvent("click", {
index: "my-index"
index: "my-index",
objectIDs: ["1"]
});
}).toThrowErrorMatchingInlineSnapshot(
`"expected required parameter \`eventName\` to be a string"`
);
}).not.toThrow();
});
it("should throw if eventName is not a string", () => {
expect(() => {
(AlgoliaInsights as any).sendEvent("click", {
eventName: 3,
index: "my-index"
index: "my-index",
objectIDs: ["1"]
});
}).toThrowErrorMatchingInlineSnapshot(
`"expected required parameter \`eventName\` to be a string"`
`"expected optional parameter \`eventName\` to be a string"`
);
});
});
Expand Down
11 changes: 2 additions & 9 deletions lib/__tests__/init.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import AlgoliaInsights from "../insights";
import { userToken } from "../_cookieUtils";
jest.mock("../_cookieUtils", () => ({ userToken: jest.fn() }));

describe("init", () => {
beforeEach(() => {
userToken.mockClear();
});
it("should throw if no parameters is passed", () => {
expect(() => {
(AlgoliaInsights as any).init();
Expand Down Expand Up @@ -65,8 +60,7 @@ describe("init", () => {
it("should use 6 months cookieDuration by default", () => {
AlgoliaInsights.init({ apiKey: "***", applicationID: "XXX" });
const month = 30 * 24 * 60 * 60 * 1000;
expect(userToken).toHaveBeenCalledTimes(1);
expect(userToken.mock.calls[0][1]).toBe(6 * month);
expect(AlgoliaInsights._cookieDuration).toBe(6 * month);
});
it.each(["not a string", 0.002, NaN])(
"should throw if cookieDuration passed but is not an integer (eg. %s)",
Expand All @@ -88,8 +82,7 @@ describe("init", () => {
applicationID: "XXX",
cookieDuration: 42
});
expect(userToken).toHaveBeenCalledTimes(1);
expect(userToken.mock.calls[0][1]).toBe(42);
expect(AlgoliaInsights._cookieDuration).toBe(42);
});
it("should set _endpointOrigin on instance to https://insights.algolia.io", () => {
AlgoliaInsights.init({ apiKey: "***", applicationID: "XXX" });
Expand Down
47 changes: 26 additions & 21 deletions lib/_cookieUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createUUID } from "./utils/index";
import { createUUID } from "./utils/uuid";
// Cookie key
const COOKIE_KEY = "_ALGOLIA";

Expand All @@ -8,7 +8,11 @@ const COOKIE_KEY = "_ALGOLIA";
* @param {[type]} cvalue [description]
* @param {[type]} exdays [description]
*/
const setCookie = (cname: string, cvalue: string, cookieDuration: number) => {
const setCookie = (
cname: string,
cvalue: number | string,
cookieDuration: number
) => {
const d = new Date();
d.setTime(d.getTime() + cookieDuration);
const expires = `expires=${d.toUTCString()}`;
Expand Down Expand Up @@ -36,25 +40,26 @@ const getCookie = (cname: string): string => {
return "";
};

/**
* Return new UUID
* @return {[string]} new UUID
*/
const checkUserIdCookie = (
userSpecifiedID?: string | number,
cookieDuration?: number
): string => {
const userToken = getCookie(COOKIE_KEY);
export const ANONYMOUS_USER_TOKEN = "ANONYMOUS_USER_TOKEN";

if (!userToken || userToken === "") {
const newUUID = createUUID();
setCookie(COOKIE_KEY, newUUID, cookieDuration);
return newUUID;
export function setUserToken(userToken: string | number): void {
if (userToken === ANONYMOUS_USER_TOKEN) {
const foundToken = getCookie(COOKIE_KEY);
if (
!foundToken ||
foundToken === "" ||
!foundToken.startsWith("anonymous-")
) {
this._userToken = `anonymous-${createUUID()}`;
setCookie(COOKIE_KEY, this._userToken, this._cookieDuration);
} else {
this._userToken = foundToken;
}
} else {
this._userToken = userToken;
}
}

return userToken;
};

const userToken = checkUserIdCookie;

export { userToken };
export function getUserToken() {
return this._userToken;
}
9 changes: 5 additions & 4 deletions lib/_sendEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export type InsightsEventType = "click" | "conversion" | "view";
export type InsightsEvent = {
eventType: InsightsEventType;

eventName: string;
eventName?: string;
userToken: string;
timestamp?: number;
index: string;
Expand Down Expand Up @@ -35,9 +35,6 @@ export function sendEvent(
}

// mandatory params
if (!isString(eventData.eventName)) {
throw TypeError("expected required parameter `eventName` to be a string");
}
if (!isString(eventData.index)) {
throw TypeError("expected required parameter `index` to be a string");
}
Expand All @@ -53,6 +50,10 @@ export function sendEvent(
};

// optional params
if (!isUndefined(eventData.eventName) && !isString(eventData.eventName)) {
throw TypeError("expected optional parameter `eventName` to be a string");
}

if (!isUndefined(eventData.timestamp)) {
if (!isNumber(eventData.timestamp)) {
throw TypeError("expected optional parameter `timestamp` to be a number");
Expand Down
7 changes: 3 additions & 4 deletions lib/click.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { InsightsEvent } from "./_sendEvent";

export interface InsightsSearchClickEvent {
eventName: string;
userToken: string;
userToken?: string;
timestamp?: number;
index: string;

Expand Down Expand Up @@ -42,7 +41,7 @@ export function clickedObjectIDsAfterSearch(params: InsightsSearchClickEvent) {

export interface InsightsClickObjectIDsEvent {
eventName: string;
userToken: string;
userToken?: string;
timestamp?: number;
index: string;

Expand Down Expand Up @@ -70,7 +69,7 @@ export function clickedObjectIDs(params: InsightsClickObjectIDsEvent) {

export interface InsightsClickFiltersEvent {
eventName: string;
userToken: string;
userToken?: string;
timestamp?: number;
index: string;

Expand Down
7 changes: 3 additions & 4 deletions lib/conversion.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
export interface InsightsSearchConversionEvent {
eventName: string;
userToken: string;
userToken?: string;
timestamp?: number;
index: string;

Expand Down Expand Up @@ -36,7 +35,7 @@ export function convertedObjectIDsAfterSearch(

export interface InsightsSearchConversionObjectIDsEvent {
eventName: string;
userToken: string;
userToken?: string;
timestamp?: number;
index: string;

Expand Down Expand Up @@ -66,7 +65,7 @@ export function convertedObjectIDs(

export interface InsightsSearchConversionFiltersEvent {
eventName: string;
userToken: string;
userToken?: string;
timestamp?: number;
index: string;

Expand Down
14 changes: 6 additions & 8 deletions lib/init.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { isUndefined, isString } from "./utils/index";
import { userToken } from "./_cookieUtils";
import { isNumber } from "util";

type InsightRegion = "de" | "us";
Expand Down Expand Up @@ -46,7 +45,9 @@ export function init(options: InitParams) {
}
if (
!isUndefined(options.cookieDuration) &&
(!isNumber(options.cookieDuration) || !isFinite(options.cookieDuration) || Math.floor(options.cookieDuration) !== options.cookieDuration)
(!isNumber(options.cookieDuration) ||
!isFinite(options.cookieDuration) ||
Math.floor(options.cookieDuration) !== options.cookieDuration)
) {
throw new Error(
`optional cookieDuration is incorrect, expected an integer`
Expand All @@ -61,12 +62,9 @@ export function init(options: InitParams) {
? `https://insights.${options.region}.algolia.io`
: "https://insights.algolia.io";

// Set hasCredentials
this._hasCredentials = true;

const cookieDuration = options.cookieDuration
this._cookieDuration = options.cookieDuration
? options.cookieDuration
: 6 * MONTH;

this._userToken = userToken(null, cookieDuration);
// Set hasCredentials
this._hasCredentials = true;
}
19 changes: 17 additions & 2 deletions lib/insights.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
InsightsSearchViewFiltersEvent,
viewedFilters
} from "./view";
import { ANONYMOUS_USER_TOKEN, getUserToken, setUserToken } from "./_cookieUtils";

type Queue = {
queue: string[][];
Expand Down Expand Up @@ -75,7 +76,14 @@ class AlgoliaAnalytics {
// Public methods
public init: (params: InitParams) => void;
public initSearch: (params: InitSearchParams) => void;
public clickedObjectIDsAfterSearch: (params?: InsightsSearchClickEvent) => void;

public ANONYMOUS_USER_TOKEN: string;
public setUserToken: (userToken: string) => void;
public getUserToken: () => string;

public clickedObjectIDsAfterSearch: (
params?: InsightsSearchClickEvent
) => void;
public clickedObjectIDs: (params?: InsightsClickObjectIDsEvent) => void;
public clickedFilters: (params?: InsightsClickFiltersEvent) => void;
public convertedObjectIDsAfterSearch: (
Expand Down Expand Up @@ -110,17 +118,24 @@ class AlgoliaAnalytics {
this.init = init.bind(this);
this.initSearch = initSearch.bind(this);

this.ANONYMOUS_USER_TOKEN = ANONYMOUS_USER_TOKEN;
this.setUserToken = setUserToken.bind(this);
this.getUserToken = getUserToken.bind(this);

this.clickedObjectIDsAfterSearch = clickedObjectIDsAfterSearch.bind(this);
this.clickedObjectIDs = clickedObjectIDs.bind(this);
this.clickedFilters = clickedFilters.bind(this);

this.convertedObjectIDsAfterSearch = convertedObjectIDsAfterSearch.bind(this);
this.convertedObjectIDsAfterSearch = convertedObjectIDsAfterSearch.bind(
this
);
this.convertedObjectIDs = convertedObjectIDs.bind(this);
this.convertedFilters = convertedFilters.bind(this);

this.viewedObjectIDs = viewedObjectIDs.bind(this);
this.viewedFilters = viewedFilters.bind(this);

this.setUserToken(this.ANONYMOUS_USER_TOKEN);
// Process queue upon script execution
this.processQueue();
}
Expand Down
6 changes: 0 additions & 6 deletions lib/utils/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,3 @@ describe("isFunction", () => {
expect(isFunction(input)).toEqual(expected);
});
});

describe("createUUID", () => {
it("should return a string composed of valid hex characters or dashes `-`", () => {
expect(createUUID()).toMatch(/^[0-9a-f\-]+$/);
});
});
Loading

0 comments on commit f7d63ee

Please sign in to comment.