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: Cache identity responses #819

1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ const Constants = {
EventBatchingIntervalMillis: 'eventBatchingIntervalMillis',
OfflineStorage: 'offlineStorage',
DirectUrlRouting: 'directURLRouting',
CacheIdentity: 'cacheIdentity',
},
DefaultInstance: 'default_instance',
CCPAPurpose: 'data_sale_opt_out',
Expand Down
197 changes: 197 additions & 0 deletions src/identity-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import Constants from './constants';
import { Dictionary, parseNumber } from './utils';
import { LocalStorageVault } from './vault';
import Types from './types';
import { IdentityApiData, UserIdentities } from '@mparticle/web-sdk';
import { IdentityAPIMethod } from './validators';
import { isObject } from './utils';
const { Identify, Modify, Login, Logout } = Constants.IdentityMethods;
import { MParticleWebSDK } from './sdkRuntimeModels';

const CACHE_HEADER = 'x-mp-max-age';
rmi22186 marked this conversation as resolved.
Show resolved Hide resolved
const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
const MILLIS_IN_ONE_SEC = 1000;
rmi22186 marked this conversation as resolved.
Show resolved Hide resolved
rmi22186 marked this conversation as resolved.
Show resolved Hide resolved
export interface IKnownIdentities extends UserIdentities {
device_application_stamp?: string;
}

export interface ICachedIdentityCall extends UserIdentities {
responseText: string;
status: number;
expireTimestamp: number;
}

export const cacheOrClearIdCache = (
method: string,
knownIdentities: IKnownIdentities,
idCache: LocalStorageVault<Dictionary>,
xhr: XMLHttpRequest
): void => {
// default the expire timestamp to one day in milliseconds unless a header comes back
let expireTimestamp = new Date().getTime() + ONE_DAY_IN_SECONDS * MILLIS_IN_ONE_SEC;

if (xhr.getAllResponseHeaders().includes(CACHE_HEADER)) {
expireTimestamp =
parseNumber(xhr.getResponseHeader(CACHE_HEADER)) * MILLIS_IN_ONE_SEC;
}

switch (method) {
case Login:
case Identify:
cacheIdentityRequest(
method,
knownIdentities,
expireTimestamp,
idCache,
xhr
);
break;
case Modify:
case Logout:
idCache.purge();
break;
}
}

export const cacheIdentityRequest = (
method: IdentityAPIMethod,
identities: IKnownIdentities,
expireTimestamp: number,
idCache: LocalStorageVault<Dictionary>,
xhr: XMLHttpRequest
): void => {
alexs-mparticle marked this conversation as resolved.
Show resolved Hide resolved
let cache: Dictionary<ICachedIdentityCall> = idCache.retrieve() || {};
let cacheKey = concatenateIdentities(method, identities);
rmi22186 marked this conversation as resolved.
Show resolved Hide resolved

cache[cacheKey] = { responseText: xhr.responseText, status: xhr.status, expireTimestamp};
idCache.store(cache);
};

// We need to ensure that identities are concatenated in a deterministic way, so
// we sort the identities based on their enum.
// we create an array, set the user identity at the index of the user identity type
alexs-mparticle marked this conversation as resolved.
Show resolved Hide resolved
export const concatenateIdentities = (
method: IdentityAPIMethod,
userIdentities: IKnownIdentities
): string => {
// set DAS first since it is not an official identity type
let cacheKey: string = `${method}:device_application_stamp=${userIdentities.device_application_stamp};`;
rmi22186 marked this conversation as resolved.
Show resolved Hide resolved
const idLength: number = Object.keys(userIdentities).length;
rmi22186 marked this conversation as resolved.
Show resolved Hide resolved
let concatenatedIdentities: string = '';

if (idLength) {
let userIDArray: Array<string> = new Array();
// create an array where each index is equal to the user identity type
for (let key in userIdentities) {
if (key === 'device_application_stamp') {
rmi22186 marked this conversation as resolved.
Show resolved Hide resolved
continue;
} else {
userIDArray[Types.IdentityType.getIdentityType(key)] =
userIdentities[key];
}
}

concatenatedIdentities = userIDArray.reduce(
(prevValue: string, currentValue: string, index: number) => {
let idName: string = Types.IdentityType.getIdentityName(index);
rmi22186 marked this conversation as resolved.
Show resolved Hide resolved
return `${prevValue}${idName}=${currentValue};`;
},
cacheKey
);
}

return concatenatedIdentities;
};

export const hasValidCachedIdentity = (
method: IdentityAPIMethod,
proposedUserIdentities: IKnownIdentities,
idCache?: LocalStorageVault<Dictionary>
): Boolean => {
// There is an edge case where multiple identity calls are taking place
// before identify fires, so there may not be a cache. See what happens when
// the ? in idCache is removed to the following test
// "queued events contain login mpid instead of identify mpid when calling
// login immediately after mParticle initializes"
const cache = idCache?.retrieve();
rmi22186 marked this conversation as resolved.
Show resolved Hide resolved

// if there is no cache, then there is no valid cached identity
if (!cache) {
return false;
}

const cacheKey: string = concatenateIdentities(
method,
proposedUserIdentities
);

// if cache doesn't have the cacheKey, there is no valid cached identity
if (!cache.hasOwnProperty(cacheKey)) {
return false;
}

// If there is a valid cache key, compare the expireTimestamp to the current time.
// If the current time is greater than the expireTimestamp, it is not a valid
// cached identity.
const expireTimestamp = cache[cacheKey].expireTimestamp;

if (expireTimestamp < new Date().getTime()) {
return false;
} else {
return true;
}
};

export const getCachedIdentity = (
method: IdentityAPIMethod,
proposedUserIdentities: IKnownIdentities,
idCache: LocalStorageVault<Dictionary>
): Dictionary<string | number | boolean> | null => {
const cacheKey: string = concatenateIdentities(
method,
proposedUserIdentities
);

const cache = idCache.retrieve();
const cachedIdentity = cache ? cache[cacheKey] : null;

return cachedIdentity;
};

export const createKnownIdentities = (
rmi22186 marked this conversation as resolved.
Show resolved Hide resolved
identityApiData: IdentityApiData,
deviceId: string
): IKnownIdentities => {
var identitiesResult: IKnownIdentities = {};
rmi22186 marked this conversation as resolved.
Show resolved Hide resolved

if (
identityApiData &&
identityApiData.userIdentities &&
isObject(identityApiData.userIdentities)
rmi22186 marked this conversation as resolved.
Show resolved Hide resolved
) {
for (var identity in identityApiData.userIdentities) {
identitiesResult[identity] =
identityApiData.userIdentities[identity];
}
}
identitiesResult.device_application_stamp = deviceId;

return identitiesResult;
};

export const removeExpiredIdentityCacheDates = (idCache: LocalStorageVault<Dictionary>): void => {
const cache: Dictionary<ICachedIdentityCall> = idCache.retrieve() || {};

idCache.purge();

const currentTime:number = new Date().getTime();
rmi22186 marked this conversation as resolved.
Show resolved Hide resolved

// Iterate over the cache and remove any key/value pairs that are expired
for (let key in cache) {
if (cache[key].expireTimestamp < currentTime) {
delete cache[key];
}
};

idCache.store(cache);
}
Loading
Loading