Files
dndbeyond_src/ddb_main/node_modules/@dndbeyond/event-pipeline-lib/client/index.js
T

427 lines
14 KiB
JavaScript

var Connection;
(function (Connection) {
Connection["Wifi"] = "wifi";
Connection["Cellular"] = "cellular";
Connection["Offline"] = "offline";
})(Connection || (Connection = {}));
var ContentType;
(function (ContentType) {
ContentType["Dice"] = "Dice";
ContentType["Source"] = "Source";
ContentType["Compendium"] = "Compendium";
ContentType["Single"] = "Single";
ContentType["Bundle"] = "Bundle";
})(ContentType || (ContentType = {}));
var Device;
(function (Device) {
Device["Tablet"] = "tablet";
Device["Smartphone"] = "smartphone";
Device["Unknown"] = "unknown";
})(Device || (Device = {}));
var Orientation;
(function (Orientation) {
Orientation["Portrait"] = "portrait";
Orientation["Landscape"] = "landscape";
Orientation["Unknown"] = "unknown";
})(Orientation || (Orientation = {}));
var Platform;
(function (Platform) {
Platform["Sigil"] = "Sigil";
Platform["Web"] = "Web";
Platform["MobileAndroid"] = "MobileAndroid";
Platform["MobileIos"] = "MobileIOS";
Platform["MobileWeb"] = "MobileWeb";
})(Platform || (Platform = {}));
var Region;
(function (Region) {
Region["UsEast1"] = "us-east-1";
Region["UsEast2"] = "us-east-2";
})(Region || (Region = {}));
var Source;
(function (Source) {
Source["Web"] = "ddb.web";
Source["Mobile"] = "ddb.mobile";
})(Source || (Source = {}));
var SubContentType;
(function (SubContentType) {
SubContentType["Family"] = "Family";
SubContentType["Set"] = "Set";
SubContentType["Rule"] = "Rule";
SubContentType["Adventure"] = "Adventure";
SubContentType["Map"] = "Map";
SubContentType["Legendary"] = "Legendary";
SubContentType["Source"] = "Source";
SubContentType["MagicItem"] = "MagicItem";
SubContentType["Monster"] = "Monster";
SubContentType["Feat"] = "Feat";
SubContentType["Background"] = "Background";
SubContentType["Subrace"] = "Subrace";
SubContentType["Class"] = "Class";
SubContentType["Subclass"] = "Subclass";
SubContentType["Spell"] = "Spell";
SubContentType["Vehicle"] = "Vehicle";
SubContentType["Race"] = "Race";
})(SubContentType || (SubContentType = {}));
var SubscriptionLevel;
(function (SubscriptionLevel) {
SubscriptionLevel["Visitor"] = "Visitor";
SubscriptionLevel["RegisteredUser"] = "Registered User";
SubscriptionLevel["Hero"] = "Hero";
SubscriptionLevel["Master"] = "Master";
SubscriptionLevel["Legendary"] = "Legendary";
})(SubscriptionLevel || (SubscriptionLevel = {}));
const byteToHex = [];
for (let i = 0; i < 256; ++i) {
byteToHex.push((i + 0x100).toString(16).slice(1));
}
function unsafeStringify(arr, offset = 0) {
return (byteToHex[arr[offset + 0]] +
byteToHex[arr[offset + 1]] +
byteToHex[arr[offset + 2]] +
byteToHex[arr[offset + 3]] +
'-' +
byteToHex[arr[offset + 4]] +
byteToHex[arr[offset + 5]] +
'-' +
byteToHex[arr[offset + 6]] +
byteToHex[arr[offset + 7]] +
'-' +
byteToHex[arr[offset + 8]] +
byteToHex[arr[offset + 9]] +
'-' +
byteToHex[arr[offset + 10]] +
byteToHex[arr[offset + 11]] +
byteToHex[arr[offset + 12]] +
byteToHex[arr[offset + 13]] +
byteToHex[arr[offset + 14]] +
byteToHex[arr[offset + 15]]).toLowerCase();
}
let getRandomValues;
const rnds8 = new Uint8Array(16);
function rng() {
if (!getRandomValues) {
if (typeof crypto === 'undefined' || !crypto.getRandomValues) {
throw new Error('crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported');
}
getRandomValues = crypto.getRandomValues.bind(crypto);
}
return getRandomValues(rnds8);
}
const randomUUID = typeof crypto !== 'undefined' && crypto.randomUUID && crypto.randomUUID.bind(crypto);
var native = { randomUUID };
function _v4(options, buf, offset) {
options = options || {};
const rnds = options.random ?? options.rng?.() ?? rng();
if (rnds.length < 16) {
throw new Error('Random bytes length must be >= 16');
}
rnds[6] = (rnds[6] & 0x0f) | 0x40;
rnds[8] = (rnds[8] & 0x3f) | 0x80;
return unsafeStringify(rnds);
}
function v4(options, buf, offset) {
if (native.randomUUID && true && !options) {
return native.randomUUID();
}
return _v4(options);
}
function flattenPlugins(event) {
const newObj = {};
Object.keys(event).forEach((key) => {
if (key === "plugins" && Array.isArray(event[key])) {
event[key].forEach((plugin) => {
Object.keys(plugin).forEach((pluginKey) => {
if (newObj[pluginKey]) {
throw new Error(`duplicate key ${pluginKey} attempting to be set!`);
}
else {
newObj[pluginKey] = plugin[pluginKey];
}
});
});
}
else {
newObj[key] = event[key];
}
});
return newObj;
}
// Regexps involved with splitting words in various case formats.
const SPLIT_LOWER_UPPER_RE = /([\p{Ll}\d])(\p{Lu})/gu;
const SPLIT_UPPER_UPPER_RE = /(\p{Lu})([\p{Lu}][\p{Ll}])/gu;
// Used to iterate over the initial split result and separate numbers.
const SPLIT_SEPARATE_NUMBER_RE = /(\d)\p{Ll}|(\p{L})\d/u;
// Regexp involved with stripping non-word characters from the result.
const DEFAULT_STRIP_REGEXP = /[^\p{L}\d]+/giu;
// The replacement value for splits.
const SPLIT_REPLACE_VALUE = "$1\0$2";
// The default characters to keep after transforming case.
const DEFAULT_PREFIX_SUFFIX_CHARACTERS = "";
/**
* Split any cased input strings into an array of words.
*/
function split(value) {
let result = value.trim();
result = result
.replace(SPLIT_LOWER_UPPER_RE, SPLIT_REPLACE_VALUE)
.replace(SPLIT_UPPER_UPPER_RE, SPLIT_REPLACE_VALUE);
result = result.replace(DEFAULT_STRIP_REGEXP, "\0");
let start = 0;
let end = result.length;
// Trim the delimiter from around the output string.
while (result.charAt(start) === "\0")
start++;
if (start === end)
return [];
while (result.charAt(end - 1) === "\0")
end--;
return result.slice(start, end).split(/\0/g);
}
/**
* Split the input string into an array of words, separating numbers.
*/
function splitSeparateNumbers(value) {
const words = split(value);
for (let i = 0; i < words.length; i++) {
const word = words[i];
const match = SPLIT_SEPARATE_NUMBER_RE.exec(word);
if (match) {
const offset = match.index + (match[1] ?? match[2]).length;
words.splice(i, 1, word.slice(0, offset), word.slice(offset));
}
}
return words;
}
/**
* Convert a string to constant case (`FOO_BAR`).
*/
function constantCase(input, options) {
const [prefix, words, suffix] = splitPrefixSuffix(input, options);
return (prefix +
words.map(upperFactory(options?.locale)).join("_") +
suffix);
}
function upperFactory(locale) {
return locale === false
? (input) => input.toUpperCase()
: (input) => input.toLocaleUpperCase(locale);
}
function splitPrefixSuffix(input, options = {}) {
const splitFn = options.split ?? (options.separateNumbers ? splitSeparateNumbers : split);
const prefixCharacters = options.prefixCharacters ?? DEFAULT_PREFIX_SUFFIX_CHARACTERS;
const suffixCharacters = options.suffixCharacters ?? DEFAULT_PREFIX_SUFFIX_CHARACTERS;
let prefixIndex = 0;
let suffixIndex = input.length;
while (prefixIndex < input.length) {
const char = input.charAt(prefixIndex);
if (!prefixCharacters.includes(char))
break;
prefixIndex++;
}
while (suffixIndex > prefixIndex) {
const index = suffixIndex - 1;
const char = input.charAt(index);
if (!suffixCharacters.includes(char))
break;
suffixIndex = index;
}
return [
input.slice(0, prefixIndex),
splitFn(input.slice(prefixIndex, suffixIndex)),
input.slice(suffixIndex),
];
}
function transformKeys(object, transformedObject = {}) {
Object.entries(object).forEach(([key, value]) => {
if (Array.isArray(value)) {
value = object[key].map((item) => typeof item === "object" ? transformKeys(item) : item);
}
else if (typeof value === "object") {
value = value ? transformKeys(value) : value;
}
transformedObject[constantCase(key)] = value ?? null;
});
return transformedObject;
}
const createEventEntry = ({ detail, detailType, eventBusName, source, }) => {
const eventTime = new Date();
const fullDetail = {
...detail,
messageId: v4(),
eventTime: eventTime.toISOString(),
};
const flattenedDetail = flattenPlugins(fullDetail);
const transformedDetail = transformKeys(flattenedDetail);
return {
Detail: JSON.stringify(transformedDetail),
DetailType: detailType,
EventBusName: eventBusName,
Source: source,
Time: eventTime,
};
};
const visitorId = "ddb_vid";
const sessionId = "ddb_sid";
/**
* Checks if the environment is a test environment.
* This is useful to avoid setting secure cookies in test environments
* where the `window` object may not be available or secure cookies are not needed.
*/
const isTestEnv = () => {
return (typeof process !== "undefined" &&
process.env &&
(process.env.NODE_ENV === "test" || process.env.VITEST));
};
const getCookie = (name) => document.cookie.match(`${name}=([^;]*)`)?.[1];
const setCookie = (name, value, opts) => {
const parts = [`${name}=${value}`, `max-age=${opts["max-age"]}`];
if (opts.secure && !isTestEnv()) {
parts.push("secure", "domain=.dndbeyond.com", "path=/");
}
return (document.cookie = parts.join(";"));
};
const deleteCookie = (name) => setCookie(name, "", { "max-age": 0, secure: !isTestEnv() });
const createSessionAndVisitorCookies = () => {
if (!getCookie(visitorId)) {
setCookie(visitorId, v4(), {
"max-age": 60 * 60 * 24 * 365, // 1 year
secure: !isTestEnv(),
});
}
if (!getCookie(sessionId)) {
setCookie(sessionId, v4(), {
"max-age": 60 * 60 * 4, // 4 hours
secure: !isTestEnv(),
});
}
else {
extendSessionCookie();
}
};
const initializeConsentCookieListener = (consentCallback) => {
const maxRetries = 10;
const retryInterval = 500;
let retries = 0;
let consentCallbackFulfilled = false;
const tryInitialize = () => {
if ("window" in globalThis && window.ketch) {
/**
* n.b. This callback will return immediately if the response is already available,
* so this acts as both an initialization and a listener for future consent changes.
*/
window.ketch("on", "consent", (consentData) => {
if (!consentCallbackFulfilled) {
consentCallbackFulfilled = true;
consentCallback?.(consentData.purposes || null);
}
if (!consentData.purposes.analytics) {
deleteCookie(visitorId);
deleteCookie(sessionId);
}
else {
createSessionAndVisitorCookies();
}
});
// For when the Ketch response never comes, we attempt a final callback
setTimeout(() => {
if (!consentCallbackFulfilled) {
console.warn("Ketch callback never fired, invoking fallback.");
consentCallbackFulfilled = true;
consentCallback?.(window.ketchConsent || null);
}
}, 5_000);
}
else if (retries < maxRetries) {
retries++;
setTimeout(tryInitialize, retryInterval);
}
else {
console.warn("Failed to initialize session cookies.");
consentCallbackFulfilled = true;
consentCallback?.(null);
}
};
tryInitialize();
};
const extendSessionCookie = () => {
const session = getCookie(sessionId);
if (session) {
setCookie(sessionId, session, {
"max-age": 60 * 60 * 4, // 4 hours
secure: !isTestEnv(),
});
}
};
class EventPipeline {
region;
/**
* Initializes the EventPipeline with the specified region and optional consent callback.
* The consent callback will return with valid consent data or null if Ketch is not available.
*
* @param region - The region for the event pipeline.
* @param consentCallback - Optional callback to handle Ketch consent data.
*/
constructor(region, consentCallback) {
this.region = region;
try {
initializeConsentCookieListener(consentCallback);
}
catch (error) {
console.warn("Failed to initialize session cookies", error);
}
}
async sendEvent(detail, source) {
try {
try {
extendSessionCookie();
}
catch (error) {
console.warn("Failed to extend session cookie", error);
}
const entry = createEventEntry({
detail,
source,
});
const response = await fetch(`https://${this.region === Region.UsEast2 ? "test-" : ""}api.dndbeyond.com/bi/events`, {
body: JSON.stringify({
Entries: [entry],
}),
headers: {
"Content-Type": "application/json",
},
method: "POST",
});
const result = {
success: response.ok,
...(response.ok
? {}
: {
errorMessage: `Send to event pipeline failed (client-bi-event)`,
}),
};
response.ok || console.error(result.errorMessage);
return result;
}
catch (error) {
console.error(error);
return {
success: false,
errorMessages: [error.message],
};
}
}
}
export { EventPipeline };