Skip to content

Commit

Permalink
feat: Implement identity caching (#834)
Browse files Browse the repository at this point in the history
  • Loading branch information
rmi22186 authored Jan 26, 2024
1 parent 8c84e26 commit 04514ec
Show file tree
Hide file tree
Showing 22 changed files with 1,398 additions and 92 deletions.
5 changes: 5 additions & 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 All @@ -181,3 +182,7 @@ const Constants = {
} as const;

export default Constants;

// https://go.mparticle.com/work/SQDSDKS-6080
export const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
export const MILLIS_IN_ONE_SEC = 1000;
247 changes: 247 additions & 0 deletions src/identity-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import Constants, { ONE_DAY_IN_SECONDS, MILLIS_IN_ONE_SEC } from './constants';
import { Dictionary, parseNumber, isObject, generateHash } from './utils';
import { BaseVault } from './vault';
import Types from './types';
import { IdentityApiData, UserIdentities, IdentityCallback } from '@mparticle/web-sdk';
import { IdentityAPIMethod, MParticleWebSDK } from './sdkRuntimeModels';

const { Identify, Modify, Login, Logout } = Constants.IdentityMethods;

export type IParseCachedIdentityResponse = (
cachedIdentity: ICachedIdentityCall,
mpid: string,
callback: IdentityCallback,
identityApiData: IdentityApiData,
identityMethod: string,
knownIdentities: IKnownIdentities,
fromCachedIdentity: boolean
) => void

export interface IKnownIdentities extends UserIdentities {
device_application_stamp?: string;
}

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

export const cacheOrClearIdCache = (
method: string,
knownIdentities: IKnownIdentities,
idCache: BaseVault<Dictionary<ICachedIdentityCall>>,
xhr: XMLHttpRequest,
parsingCachedResponse: boolean,
): void => {
// when parsing a response that has already been cached, simply return instead of attempting another cache
if (parsingCachedResponse) { return; }

const CACHE_HEADER = 'x-mp-max-age';

// default the expire timestamp to one day in milliseconds unless a header comes back
let now = new Date().getTime();
let expireTimestamp = now + ONE_DAY_IN_SECONDS * MILLIS_IN_ONE_SEC;
if (xhr.getAllResponseHeaders().includes(CACHE_HEADER)) {
expireTimestamp =
now + (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: BaseVault<Dictionary<ICachedIdentityCall>>,
xhr: XMLHttpRequest
): void => {
const cache: Dictionary<ICachedIdentityCall> = idCache.retrieve() || ({} as Dictionary<ICachedIdentityCall>);
const cacheKey = concatenateIdentities(method, identities);
const hashedKey = generateHash(cacheKey);

cache[hashedKey] = { 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
export const concatenateIdentities = (
method: IdentityAPIMethod,
userIdentities: IKnownIdentities
): string => {
const DEVICE_APPLICATION_STAMP = 'device_application_stamp';
// set DAS first since it is not an official identity type
let cacheKey: string = `${method}:${DEVICE_APPLICATION_STAMP}=${userIdentities.device_application_stamp};`;
const idLength: number = Object.keys(userIdentities).length;
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) {
continue;
} else {
userIDArray[Types.IdentityType.getIdentityType(key)] =
userIdentities[key];
}
}

concatenatedIdentities = userIDArray.reduce(
(prevValue: string, currentValue: string, index: number) => {
const idName: string = Types.IdentityType.getIdentityName(index);
return `${prevValue}${idName}=${currentValue};`;
},
cacheKey
);
}

return concatenatedIdentities;
};

export const hasValidCachedIdentity = (
method: IdentityAPIMethod,
proposedUserIdentities: IKnownIdentities,
idCache?: BaseVault<Dictionary<ICachedIdentityCall>>
): 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();

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

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

// if cache doesn't have the cacheKey, there is no valid cached identity
if (!cache.hasOwnProperty(hashedKey)) {
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[hashedKey].expireTimestamp;

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

export const getCachedIdentity = (
method: IdentityAPIMethod,
proposedUserIdentities: IKnownIdentities,
idCache: BaseVault<Dictionary<ICachedIdentityCall>>
): ICachedIdentityCall | null => {
const cacheKey: string = concatenateIdentities(
method,
proposedUserIdentities
);
const hashedKey = generateHash(cacheKey);

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

return cachedIdentity;
};

// https://go.mparticle.com/work/SQDSDKS-6079
export const createKnownIdentities = (
identityApiData: IdentityApiData,
deviceId: string
): IKnownIdentities => {
const identitiesResult: IKnownIdentities = {};

if (isObject(identityApiData?.userIdentities)) {
for (let identity in identityApiData.userIdentities) {
identitiesResult[identity] =
identityApiData.userIdentities[identity];
}
}
identitiesResult.device_application_stamp = deviceId;

return identitiesResult;
};

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

const currentTime: number = new Date().getTime();

// 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);
}

export const tryCacheIdentity = (
knownIdentities: IKnownIdentities,
idCache: BaseVault<Dictionary<ICachedIdentityCall>>,
parseIdentityResponse: IParseCachedIdentityResponse,
mpid: string,
callback: IdentityCallback,
identityApiData: IdentityApiData,
identityMethod: IdentityAPIMethod
): boolean => {
// https://go.mparticle.com/work/SQDSDKS-6095
const shouldReturnCachedIdentity = hasValidCachedIdentity(
identityMethod,
knownIdentities,
idCache
);

// If Identity is cached, then immediately parse the identity response
if (shouldReturnCachedIdentity) {
const cachedIdentity = getCachedIdentity(
identityMethod,
knownIdentities,
idCache
);

parseIdentityResponse(
cachedIdentity,
mpid,
callback,
identityApiData,
identityMethod,
knownIdentities,
true
);

return true;
}
return false;
}
Loading

0 comments on commit 04514ec

Please sign in to comment.