diff --git a/README.md b/README.md index c66b10e..88bc78d 100644 --- a/README.md +++ b/README.md @@ -209,16 +209,16 @@ Will result in sending these Event Properties: - `value`: `25.0` - `quantity`: `4` -- `contents`: [{ - id: '1111product', - item_price: 10, - quantity: 2 - }, - { - id: '2222product', - item_price: 5, - quantity: 2 - }] +- `contents`: `[{ +id: '1111product', +item_price: 10, +quantity: 2 +}, +{ +id: '2222product', +item_price: 5, +quantity: 2 +}]` ## 📝 License diff --git a/src/index.ts b/src/index.ts index 3fb9b0d..28f0a0e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,10 +3,24 @@ import UAParser from 'ua-parser-js' import { checkEventName, hashPayload, - pushEventData, - pushCustomData, + enrichEventData, + getCustomData, } from './utils' +export interface Product { + product_id: number | string + sku: number | string + name: string + category: string + brand: string + price: number | string + quantity: number + variant: string + currency: string + value: number | string + position: number | string +} + export const getEventData = async ( event: MCEvent, pageview: boolean, @@ -15,18 +29,19 @@ export const getEventData = async ( const { client } = event const parsedUserAgent = UAParser(client.userAgent) const payload = ecomPayload ? ecomPayload : event.payload - const hashedUserProperties = await hashPayload(payload) - const eventDataResult = await pushEventData(payload) - const customDataResult = await pushCustomData(payload) + const [hashedUserProperties, eventDataResult, customDataResult] = + await Promise.all([ + hashPayload(payload), + enrichEventData(payload), + getCustomData(payload), + ]) const eventData = { event_name: pageview ? 'page_visit' : payload.name, action_source: payload.action_source || 'web', event_time: Math.floor(Date.now() / 1000), event_id: - payload.event_id || - payload.ecommerce?.event_id || - String(Math.round(Math.random() * 100000000000000000)), + payload.event_id || payload.ecommerce?.event_id || crypto.randomUUID(), event_source_url: payload.event_source_url || client.url.href, opt_out: payload.opt_out || false, device_brand: parsedUserAgent.device.vendor, @@ -49,48 +64,47 @@ export const getEventData = async ( export default async function (manager: Manager, settings: ComponentSettings) { const getEcommercePayload = (event: MCEvent) => { - const { type, name } = event + const { name } = event let { payload } = event payload = { ...payload, ...payload.ecommerce } - if (type === 'ecommerce') { - const mapEventName = (name: string | undefined) => { - if (name === 'Product Added') { - // Fixed the assignment (=) to comparison (===) - return 'add_to_cart' - } else if (name === 'Order Completed') { - return 'checkout' - } - } - - payload.name = mapEventName(name) - if (Array.isArray(payload.products)) { - payload.content_ids = payload.products - .map((product: any) => product.product_id) - .join() - payload.content_name = payload.products - .map((product: any) => product.name) - .join() - payload.content_category = payload.products - .map((product: any) => product.category) - .join() - payload.content_brand = payload.products - .map((product: any) => product.brand) - .join() - payload.contents = payload.products.map((product: any) => ({ - id: product.product_id, - item_price: product.price.toString(), - quantity: product.quantity, - })) - payload.num_items = - payload.quantity || - payload.products.reduce( - (sum: number, product: any) => sum + parseInt(product.quantity, 10), - 0 - ) - } - - payload.value = payload.revenue || payload.total || payload.value + payload.name = + name === 'Product Added' + ? 'add_to_cart' + : name === 'Order Completed' + ? 'checkout' + : name + if (Array.isArray(payload.products)) { + payload.content_ids = payload.products + .map((product: Product) => product.product_id) + .join() + payload.content_name = payload.products + .map((product: Product) => product.name) + .join() + payload.content_category = payload.products + .map((product: Product) => product.category) + .join() + payload.content_brand = payload.products + .map((product: Product) => product.brand) + .join() + payload.contents = payload.products.map((product: any) => ({ + id: product.product_id, + item_price: product.price.toString(), + quantity: product.quantity, + })) + payload.num_items = + payload.quantity || + payload.products.reduce((sum: number, product: Product) => { + if (typeof product.quantity === 'string') { + return sum + parseInt(product.quantity, 10) + } else if (typeof product.quantity === 'number') { + return sum + product.quantity + } + return sum + }, 0) } + + payload.value = payload.revenue || payload.total || payload.value + console.log(`this is ecom payload: $$$$`, payload) return payload } diff --git a/src/utils.ts b/src/utils.ts index e142763..4a1201c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,15 +1,17 @@ import { MCEvent } from '@managed-components/types' import crypto from 'crypto' +import { Product } from './index' + +const allowedEvents = [ + 'custom', + 'lead', + 'search', + 'signup', + 'view_category', + 'watch_video', +] export function checkEventName(event: MCEvent): MCEvent | undefined { - const allowedEvents = [ - 'custom', - 'lead', - 'search', - 'signup', - 'view_category', - 'watch_video', - ] if (allowedEvents.includes(event.payload.name)) { return event } else { @@ -68,7 +70,7 @@ export async function hashPayload( return results } -export async function pushEventData(payload: Record) { +export async function enrichEventData(payload: Record) { const eventDataKeys = [ 'partner_name', 'app_id', @@ -86,7 +88,7 @@ export async function pushEventData(payload: Record) { return eventDataResult } -export async function pushCustomData(payload: Record) { +export async function getCustomData(payload: Record) { const customDataKeys = [ 'search_string', 'opt_out_type', @@ -153,28 +155,31 @@ export async function getEcommercePayload(event: MCEvent) { payload.name = mapEventName(name) if (Array.isArray(payload.products)) { payload.content_ids = payload.products - .map((product: any) => product.product_id) + .map((product: Product) => product.product_id) .join() payload.content_name = payload.products - .map((product: any) => product.name) + .map((product: Product) => product.name) .join() payload.content_category = payload.products - .map((product: any) => product.category) + .map((product: Product) => product.category) .join() payload.content_brand = payload.products - .map((product: any) => product.brand) + .map((product: Product) => product.brand) .join() - payload.contents = payload.products.map((product: any) => ({ + payload.contents = payload.products.map((product: Product) => ({ id: product.product_id, item_price: product.price.toString(), quantity: product.quantity, })) payload.num_items = payload.quantity || - payload.products.reduce( - (sum: any, product: any) => sum + parseInt(product.quantity, 10), - 0 - ) + payload.products.reduce((sum: number, product: Product) => { + if (typeof product.quantity === 'string') { + return sum + parseInt(product.quantity, 10) + } else { + return sum + product.quantity + } + }, 0) } payload.value = payload.revenue || payload.total || payload.value diff --git a/tsconfig.json b/tsconfig.json index f94a71b..10684af 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,102 +1,23 @@ { "compilerOptions": { /* Visit https://aka.ms/tsconfig.json to read more about this file */ - - /* Projects */ - // "incremental": true, /* Enable incremental compilation */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - /* Language and Environment */ "target": "es2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ - // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - /* Modules */ "module": "commonjs" /* Specify what module code is generated. */, - // "rootDir": "./", /* Specify the root folder within your source files. */ "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ "types": [ "vitest/globals" ] /* Specify type package names to be included without being referenced in a source file. */, - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ "resolveJsonModule": true /* Enable importing .json files */, - // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ "outDir": "./dist" /* Specify an output folder for all emitted files. */, - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - /* Type Checking */ "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ - // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ - // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ } }