@mparticle/web-sdk
Version:
mParticle core SDK for web applications
527 lines (478 loc) • 18.9 kB
text/typescript
import {
CCPAConsentState,
ConsentState,
GDPRConsentState,
PrivacyConsentState,
} from '@mparticle/web-sdk';
import { Dictionary, isObject } from './utils';
import KitFilterHelper from './kitFilterHelper';
import Constants from './constants';
import { IMParticleUser } from './identity-user-interfaces';
import { IMParticleWebSDKInstance } from './mp-instance';
const { CCPAPurpose } = Constants;
export interface IMinifiedConsentJSONObject {
gdpr?: Dictionary<IPrivacyV2DTO>;
ccpa?: {
[CCPAPurpose]: IPrivacyV2DTO;
};
}
export interface ICreatePrivacyConsentFunction {
(
consented: boolean,
timestamp?: number,
consentDocument?: string,
location?: string,
hardwareId?: string
): PrivacyConsentState | null;
}
// Represents Consent API defined as part of mPInstance
// TODO: Refactor this with Consent Namespace in @types/mparticle-web-sdk
// https://go.mparticle.com/work/SQDSDKS-5009
export interface SDKConsentApi {
createGDPRConsent: ICreatePrivacyConsentFunction;
createCCPAConsent: ICreatePrivacyConsentFunction;
createConsentState: (consentState?: ConsentState) => ConsentState;
}
export interface IConsentSerialization {
toMinifiedJsonObject: (state: ConsentState) => IMinifiedConsentJSONObject;
fromMinifiedJsonObject: (json: IMinifiedConsentJSONObject) => ConsentState;
}
// TODO: Resolve discrepency between ConsentState and SDKConsentState
// https://go.mparticle.com/work/SQDSDKS-5009
export interface SDKConsentState
extends Omit<ConsentState, 'getGDPRConsentState' | 'getCCPAConsentState'> {
getGDPRConsentState(): SDKGDPRConsentState;
getCCPAConsentState(): SDKCCPAConsentState;
}
// TODO: Resolve discrepency between ConsentState and SDKConsentState
// https://go.mparticle.com/work/SQDSDKS-5009
// Specifically PrivacyConsentState, GDPRConsentState and CCPAConsentState
// Treat all attributes as required, but we had to override this
// to be optional for a bugfix:
// https://github.com/mParticle/mparticle-web-sdk/commit/3b11ead79f25b417737442a4fabd6435750f46b8
export interface SDKConsentStateData {
Consented: boolean;
Timestamp?: number;
ConsentDocument?: string;
Location?: string;
HardwareId?: string;
}
export type SDKGDPRConsentState = Dictionary<SDKConsentStateData>;
export interface SDKCCPAConsentState extends SDKConsentStateData {}
export interface IPrivacyV2DTO {
c: boolean; // Consented
ts: number; // Timestamp
d: string; // Document
l: string; // Location
h: string; // HardwareID
}
export type IGDPRConsentStateV2DTO = Dictionary<IPrivacyV2DTO>;
export interface ICCPAConsentStateV2DTO {
[CCPAPurpose]: IPrivacyV2DTO;
}
// TODO: Remove when Deprecating V2
export interface IConsentStateV2DTO {
gdpr?: IGDPRConsentStateV2DTO;
ccpa?: ICCPAConsentStateV2DTO;
}
export interface IConsentRulesValues {
consentPurpose: string;
hasConsented: boolean;
}
export interface IConsentRules {
includeOnMatch: boolean;
values: IConsentRulesValues[];
}
// TODO: Remove this if we can safely deprecate `removeCCPAState`
export interface IConsentState extends ConsentState {
removeCCPAState: () => ConsentState;
}
// Represents Actual Interface for Consent Module
// TODO: Should eventually consolidate with SDKConsentStateApi
export interface IConsent {
isEnabledForUserConsent: (
consentRules: IConsentRules,
user: IMParticleUser
) => boolean;
createPrivacyConsent: ICreatePrivacyConsentFunction;
createConsentState: (consentState?: ConsentState) => ConsentState;
ConsentSerialization: IConsentSerialization;
}
export default function Consent(this: IConsent, mpInstance: IMParticleWebSDKInstance) {
const self = this;
// this function is called when consent is required to
// determine if a cookie sync should happen, or a
// forwarder should be initialized
this.isEnabledForUserConsent = function(
consentRules: IConsentRules,
user: IMParticleUser
): boolean {
if (
!consentRules ||
!consentRules.values ||
!consentRules.values.length
) {
return true;
}
if (!user) {
return false;
}
const purposeHashes: Dictionary<boolean> = {};
const consentState: SDKConsentState = user.getConsentState();
let purposeHash: string;
if (consentState) {
// the server hashes consent purposes in the following way:
// GDPR - '1' + purpose name
// CCPA - '2data_sale_opt_out' (there is only 1 purpose of data_sale_opt_out for CCPA)
const GDPRConsentHashPrefix = '1';
const CCPAHashPrefix = '2';
const gdprConsentState = consentState.getGDPRConsentState();
if (gdprConsentState) {
for (const purpose in gdprConsentState) {
if (gdprConsentState.hasOwnProperty(purpose)) {
purposeHash = KitFilterHelper.hashConsentPurposeConditionalForwarding(GDPRConsentHashPrefix, purpose);
purposeHashes[purposeHash] =
gdprConsentState[purpose].Consented;
}
}
}
const CCPAConsentState = consentState.getCCPAConsentState();
if (CCPAConsentState) {
purposeHash = KitFilterHelper.hashConsentPurposeConditionalForwarding(CCPAHashPrefix, CCPAPurpose);
purposeHashes[purposeHash] = CCPAConsentState.Consented;
}
}
const isMatch = consentRules.values.some(function(consentRule) {
const consentPurposeHash = consentRule.consentPurpose;
const hasConsented = consentRule.hasConsented;
if (purposeHashes.hasOwnProperty(consentPurposeHash)) {
return purposeHashes[consentPurposeHash] === hasConsented;
}
return false;
});
return consentRules.includeOnMatch === isMatch;
};
this.createPrivacyConsent = function(
consented: boolean,
timestamp?: number,
consentDocument?: string,
location?: string,
hardwareId?: string
): PrivacyConsentState | null {
if (typeof consented !== 'boolean') {
mpInstance.Logger.error(
'Consented boolean is required when constructing a Consent object.'
);
return null;
}
if (timestamp && isNaN(timestamp)) {
mpInstance.Logger.error(
'Timestamp must be a valid number when constructing a Consent object.'
);
return null;
}
if (consentDocument && typeof consentDocument !== 'string') {
mpInstance.Logger.error(
'Document must be a valid string when constructing a Consent object.'
);
return null;
}
if (location && typeof location !== 'string') {
mpInstance.Logger.error(
'Location must be a valid string when constructing a Consent object.'
);
return null;
}
if (hardwareId && typeof hardwareId !== 'string') {
mpInstance.Logger.error(
'Hardware ID must be a valid string when constructing a Consent object.'
);
return null;
}
return {
Consented: consented,
Timestamp: timestamp || Date.now(),
ConsentDocument: consentDocument,
Location: location,
HardwareId: hardwareId,
};
};
this.ConsentSerialization = {
toMinifiedJsonObject(state: ConsentState): IMinifiedConsentJSONObject {
const jsonObject: Partial<IMinifiedConsentJSONObject> = {};
if (state) {
const gdprConsentState = state.getGDPRConsentState();
if (gdprConsentState) {
jsonObject.gdpr = {} as Dictionary<IPrivacyV2DTO>;
for (const purpose in gdprConsentState) {
if (gdprConsentState.hasOwnProperty(purpose)) {
const gdprConsent = gdprConsentState[purpose];
jsonObject.gdpr[purpose] = {} as IPrivacyV2DTO;
if (typeof gdprConsent.Consented === 'boolean') {
jsonObject.gdpr[purpose].c =
gdprConsent.Consented;
}
if (typeof gdprConsent.Timestamp === 'number') {
jsonObject.gdpr[purpose].ts =
gdprConsent.Timestamp;
}
if (
typeof gdprConsent.ConsentDocument === 'string'
) {
jsonObject.gdpr[purpose].d =
gdprConsent.ConsentDocument;
}
if (typeof gdprConsent.Location === 'string') {
jsonObject.gdpr[purpose].l =
gdprConsent.Location;
}
if (typeof gdprConsent.HardwareId === 'string') {
jsonObject.gdpr[purpose].h =
gdprConsent.HardwareId;
}
}
}
}
const ccpaConsentState = state.getCCPAConsentState();
if (ccpaConsentState) {
jsonObject.ccpa = {
[CCPAPurpose]: {} as IPrivacyV2DTO,
};
if (typeof ccpaConsentState.Consented === 'boolean') {
jsonObject.ccpa[CCPAPurpose].c =
ccpaConsentState.Consented;
}
if (typeof ccpaConsentState.Timestamp === 'number') {
jsonObject.ccpa[CCPAPurpose].ts =
ccpaConsentState.Timestamp;
}
if (typeof ccpaConsentState.ConsentDocument === 'string') {
jsonObject.ccpa[CCPAPurpose].d =
ccpaConsentState.ConsentDocument;
}
if (typeof ccpaConsentState.Location === 'string') {
jsonObject.ccpa[CCPAPurpose].l =
ccpaConsentState.Location;
}
if (typeof ccpaConsentState.HardwareId === 'string') {
jsonObject.ccpa[CCPAPurpose].h =
ccpaConsentState.HardwareId;
}
}
}
return jsonObject;
},
fromMinifiedJsonObject(json: IMinifiedConsentJSONObject): ConsentState {
const state: ConsentState = self.createConsentState();
if (json.gdpr) {
for (const purpose in json.gdpr) {
if (json.gdpr.hasOwnProperty(purpose)) {
const gdprConsent = self.createPrivacyConsent(
json.gdpr[purpose].c,
json.gdpr[purpose].ts,
json.gdpr[purpose].d,
json.gdpr[purpose].l,
json.gdpr[purpose].h
);
state.addGDPRConsentState(purpose, gdprConsent);
}
}
}
if (json.ccpa) {
if (json.ccpa.hasOwnProperty(CCPAPurpose)) {
const ccpaConsent = self.createPrivacyConsent(
json.ccpa[CCPAPurpose].c,
json.ccpa[CCPAPurpose].ts,
json.ccpa[CCPAPurpose].d,
json.ccpa[CCPAPurpose].l,
json.ccpa[CCPAPurpose].h
);
state.setCCPAConsentState(ccpaConsent);
}
}
return state;
},
};
// TODO: Refactor this method into a constructor
this.createConsentState = function(
this: ConsentState,
consentState?: ConsentState
): IConsentState {
let gdpr = {};
let ccpa = {};
if (consentState) {
const consentStateCopy = self.createConsentState();
consentStateCopy.setGDPRConsentState(
consentState.getGDPRConsentState()
);
consentStateCopy.setCCPAConsentState(
consentState.getCCPAConsentState()
);
// TODO: Remove casting once `removeCCPAState` is removed;
return consentStateCopy as IConsentState;
}
function canonicalizeForDeduplication(purpose: string): string {
if (typeof purpose !== 'string') {
return null;
}
const trimmedPurpose = purpose.trim();
if (!trimmedPurpose.length) {
return null;
}
return trimmedPurpose.toLowerCase();
}
/**
* Invoke these methods on a consent state object.
* <p>
* Usage: const consent = mParticle.Consent.createConsentState()
* <br>
* consent.setGDPRCoonsentState()
*
* @class Consent
*/
/**
* Add a GDPR Consent State to the consent state object
*
* @method addGDPRConsentState
* @param purpose [String] Data processing purpose that describes the type of processing done on the data subject’s data
* @param gdprConsent [Object] A GDPR consent object created via mParticle.Consent.createGDPRConsent(...)
*/
function addGDPRConsentState(
this: ConsentState,
purpose: string,
gdprConsent: PrivacyConsentState
): ConsentState {
const normalizedPurpose = canonicalizeForDeduplication(purpose);
if (!normalizedPurpose) {
mpInstance.Logger.error('Purpose must be a string.');
return this;
}
if (!isObject(gdprConsent)) {
mpInstance.Logger.error(
'Invoked with a bad or empty consent object.'
);
return this;
}
const gdprConsentCopy = self.createPrivacyConsent(
gdprConsent.Consented,
gdprConsent.Timestamp,
gdprConsent.ConsentDocument,
gdprConsent.Location,
gdprConsent.HardwareId
);
if (gdprConsentCopy) {
gdpr[normalizedPurpose] = gdprConsentCopy;
}
return this;
}
function setGDPRConsentState(
this: ConsentState,
gdprConsentState: GDPRConsentState
): ConsentState {
if (!gdprConsentState) {
gdpr = {};
} else if (isObject(gdprConsentState)) {
gdpr = {};
for (const purpose in gdprConsentState) {
if (gdprConsentState.hasOwnProperty(purpose)) {
this.addGDPRConsentState(
purpose,
gdprConsentState[purpose]
);
}
}
}
return this;
}
/**
* Remove a GDPR Consent State to the consent state object
*
* @method removeGDPRConsentState
* @param purpose [String] Data processing purpose that describes the type of processing done on the data subject’s data
*/
function removeGDPRConsentState(
this: ConsentState,
purpose: string
): ConsentState {
const normalizedPurpose = canonicalizeForDeduplication(purpose);
if (!normalizedPurpose) {
return this;
}
delete gdpr[normalizedPurpose];
return this;
}
/**
* Gets the GDPR Consent State
*
* @method getGDPRConsentState
* @return {Object} A GDPR Consent State
*/
function getGDPRConsentState(): GDPRConsentState {
return Object.assign({}, gdpr);
}
/**
* Sets a CCPA Consent state (has a single purpose of 'data_sale_opt_out')
*
* @method setCCPAConsentState
* @param {Object} ccpaConsent CCPA Consent State
*/
function setCCPAConsentState(
this: ConsentState,
ccpaConsent: CCPAConsentState
) {
if (!isObject(ccpaConsent)) {
mpInstance.Logger.error(
'Invoked with a bad or empty CCPA consent object.'
);
return this;
}
const ccpaConsentCopy = self.createPrivacyConsent(
ccpaConsent.Consented,
ccpaConsent.Timestamp,
ccpaConsent.ConsentDocument,
ccpaConsent.Location,
ccpaConsent.HardwareId
);
if (ccpaConsentCopy) {
ccpa[CCPAPurpose] = ccpaConsentCopy;
}
return this;
}
/**
* Gets the CCPA Consent State
*
* @method getCCPAConsentStatensent
* @return {Object} A CCPA Consent State
*/
function getCCPAConsentState(): CCPAConsentState {
return ccpa[CCPAPurpose];
}
/**
* Removes CCPA from the consent state object
*
* @method removeCCPAConsentState
*/
function removeCCPAConsentState(this: ConsentState) {
delete ccpa[CCPAPurpose];
return this;
}
// TODO: Can we remove this? It is deprecated.
function removeCCPAState(this: ConsentState) {
mpInstance.Logger.warning(
'removeCCPAState is deprecated and will be removed in a future release; use removeCCPAConsentState instead'
);
// @ts-ignore
return removeCCPAConsentState();
}
return {
setGDPRConsentState,
addGDPRConsentState,
setCCPAConsentState,
getCCPAConsentState,
getGDPRConsentState,
removeGDPRConsentState,
removeCCPAState, // TODO: Can we remove? This is deprecated
removeCCPAConsentState,
};
};
}