From 6eda6ca5ecac8872b238728dc0d294d9fd9b7645 Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Thu, 24 Aug 2023 16:17:00 -0400 Subject: [PATCH] Initial Implementation of Next Gen Event Mediator --- package-lock.json | 160 +++---- package.json | 10 +- src/NextGen/Component.ts | 2 + src/NextGen/EventLogging.ts | 544 +++++++++++++++++++++++ src/NextGen/Mediator.ts | 48 ++ src/apiClient.ts | 4 +- src/batchUploader.ts | 4 +- src/helpers.js | 14 +- src/mockBatchCreator.ts | 1 + src/mp-instance.js | 219 +++++++-- src/sdkRuntimeModels.ts | 31 +- src/sessionManager.js | 24 +- src/store.ts | 6 + src/types.interfaces.ts | 46 +- src/types.js | 2 + test/jest/NextGen/MPEventLogging.spec.ts | 10 + test/jest/NextGen/MPMediator.spec.ts | 76 ++++ 17 files changed, 1055 insertions(+), 146 deletions(-) create mode 100644 src/NextGen/Component.ts create mode 100644 src/NextGen/EventLogging.ts create mode 100644 src/NextGen/Mediator.ts create mode 100644 test/jest/NextGen/MPEventLogging.spec.ts create mode 100644 test/jest/NextGen/MPMediator.spec.ts diff --git a/package-lock.json b/package-lock.json index 41e247a2f..ecb15b54a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,9 +13,9 @@ "slugify": "^1.3.6" }, "devDependencies": { - "@babel/core": "^7.18.6", - "@babel/eslint-parser": "^7.18.2", - "@babel/eslint-plugin": "^7.17.7", + "@babel/core": "^7.22.1", + "@babel/eslint-parser": "^7.22.1", + "@babel/eslint-plugin": "^7.22.1", "@babel/plugin-proposal-class-properties": "^7.5.5", "@babel/plugin-proposal-object-rest-spread": "^7.5.5", "@babel/plugin-transform-runtime": "^7.6.0", @@ -42,7 +42,7 @@ "browserify": "^17.0.0", "chai": "^4.2.0", "cross-env": "^7.0.3", - "eslint": "^8.36.0", + "eslint": "^8.47.0", "eslint-config-prettier": "8.8.0", "eslint-plugin-prettier": "3.4.1", "fetch-mock": "^7.5.1", @@ -61,7 +61,7 @@ "karma-should": "^1.0.0", "mocha": "^9.0.0", "pre-commit": "^1.2.2", - "prettier": "1.18.2", + "prettier": "2.0.0", "rollup": "^2.75.7", "shelljs": "^0.8.4", "should": "^7.1.0", @@ -2010,18 +2010,18 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.4.0.tgz", - "integrity": "sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", + "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.0.tgz", - "integrity": "sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -2042,9 +2042,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -2069,9 +2069,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz", - "integrity": "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.47.0.tgz", + "integrity": "sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -7897,27 +7897,27 @@ } }, "node_modules/eslint": { - "version": "8.45.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.45.0.tgz", - "integrity": "sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.47.0.tgz", + "integrity": "sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.1.0", - "@eslint/js": "8.44.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "^8.47.0", "@humanwhocodes/config-array": "^0.11.10", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.6.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -8130,9 +8130,9 @@ } }, "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", - "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -8146,9 +8146,9 @@ } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", - "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -8227,9 +8227,9 @@ } }, "node_modules/espree": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.0.tgz", - "integrity": "sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "dependencies": { "acorn": "^8.9.0", @@ -8256,9 +8256,9 @@ } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", - "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -18088,15 +18088,15 @@ } }, "node_modules/prettier": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz", - "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.0.tgz", + "integrity": "sha512-vI55PC+GFLOVtpwr2di1mYhJF36v+kztJov8sx3AmqbfdA+2Dhozxb+3e1hTgoV9lyhnVJFF3Z8GCVeMBOS1bA==", "dev": true, "bin": { "prettier": "bin-prettier.js" }, "engines": { - "node": ">=4" + "node": ">=10.13.0" } }, "node_modules/prettier-linter-helpers": { @@ -22977,15 +22977,15 @@ } }, "@eslint-community/regexpp": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.4.0.tgz", - "integrity": "sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", + "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", "dev": true }, "@eslint/eslintrc": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.0.tgz", - "integrity": "sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -23000,9 +23000,9 @@ }, "dependencies": { "globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -23017,9 +23017,9 @@ } }, "@eslint/js": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz", - "integrity": "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.47.0.tgz", + "integrity": "sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==", "dev": true }, "@humanwhocodes/config-array": { @@ -27689,27 +27689,27 @@ } }, "eslint": { - "version": "8.45.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.45.0.tgz", - "integrity": "sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.47.0.tgz", + "integrity": "sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.1.0", - "@eslint/js": "8.44.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "^8.47.0", "@humanwhocodes/config-array": "^0.11.10", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.6.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -27774,9 +27774,9 @@ "dev": true }, "eslint-scope": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", - "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "requires": { "esrecurse": "^4.3.0", @@ -27784,9 +27784,9 @@ } }, "eslint-visitor-keys": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", - "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true }, "estraverse": { @@ -27908,9 +27908,9 @@ "dev": true }, "espree": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.0.tgz", - "integrity": "sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "requires": { "acorn": "^8.9.0", @@ -27925,9 +27925,9 @@ "dev": true }, "eslint-visitor-keys": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", - "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true } } @@ -35256,9 +35256,9 @@ "dev": true }, "prettier": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz", - "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.0.tgz", + "integrity": "sha512-vI55PC+GFLOVtpwr2di1mYhJF36v+kztJov8sx3AmqbfdA+2Dhozxb+3e1hTgoV9lyhnVJFF3Z8GCVeMBOS1bA==", "dev": true }, "prettier-linter-helpers": { diff --git a/package.json b/package.json index 1a0d6118a..e5e86112f 100644 --- a/package.json +++ b/package.json @@ -73,9 +73,9 @@ "lint" ], "devDependencies": { - "@babel/core": "^7.18.6", - "@babel/eslint-parser": "^7.18.2", - "@babel/eslint-plugin": "^7.17.7", + "@babel/core": "^7.22.1", + "@babel/eslint-parser": "^7.22.1", + "@babel/eslint-plugin": "^7.22.1", "@babel/plugin-proposal-class-properties": "^7.5.5", "@babel/plugin-proposal-object-rest-spread": "^7.5.5", "@babel/plugin-transform-runtime": "^7.6.0", @@ -102,7 +102,7 @@ "browserify": "^17.0.0", "chai": "^4.2.0", "cross-env": "^7.0.3", - "eslint": "^8.36.0", + "eslint": "^8.47.0", "eslint-config-prettier": "8.8.0", "eslint-plugin-prettier": "3.4.1", "fetch-mock": "^7.5.1", @@ -121,7 +121,7 @@ "karma-should": "^1.0.0", "mocha": "^9.0.0", "pre-commit": "^1.2.2", - "prettier": "1.18.2", + "prettier": "2.0.0", "rollup": "^2.75.7", "shelljs": "^0.8.4", "should": "^7.1.0", diff --git a/src/NextGen/Component.ts b/src/NextGen/Component.ts new file mode 100644 index 000000000..39bfb1f79 --- /dev/null +++ b/src/NextGen/Component.ts @@ -0,0 +1,2 @@ +// QUESTION: Should we make mediator a property of a component? +export interface IComponent {} diff --git a/src/NextGen/EventLogging.ts b/src/NextGen/EventLogging.ts new file mode 100644 index 000000000..1036dc5d5 --- /dev/null +++ b/src/NextGen/EventLogging.ts @@ -0,0 +1,544 @@ +import { + BaseEvent, + SDKEvent, + SDKEventCustomFlags, + SDKPromotion, +} from '../sdkRuntimeModels'; +import Mediator from './Mediator'; +import Constants from '../constants'; +import { IComponent } from './Component'; +import { IStore } from '../store'; +import { + Impression, + Logger, + Product, + PromotionType, + TransactionAttributes, +} from '@mparticle/web-sdk'; +import { + MessageType, + EventTypeEnum, + ProductActionType, + CommerceEventType, + PromotionActionType, +} from '../types.interfaces'; +import { Dictionary, isObject } from '../utils'; +import { Promotion } from '@mparticle/event-models'; + +const { Messages } = Constants; + +// TODO: We should rename/revise BaseEvent into MPEvent +export interface MPEvent extends BaseEvent {} +export interface CommerceEvent extends SDKEvent {} + +// FIXME: Add TotalAmount to Product +interface IProduct extends Product { + TotalAmount?: number; +} + +// TODO: Do we have something similar to this? +export type CustomAttributes = Dictionary; + +export interface EventOptions {} + +type TrackingCallback = ( + callback: PositionCallback, + position?: GeolocationPosition +) => void; + +export interface IEventLogging extends IComponent { + logEvent(event: MPEvent, options?: EventOptions): void; +} + +export default class EventLogging implements IEventLogging { + // TODO: Can we use the mediator instead? + public mediator: Mediator; + + private store: IStore; + private logger: Logger; + + constructor(mediator: Mediator) { + this.mediator = mediator; + this.store = mediator.store; + this.logger = mediator.logger; + } + + logEvent(event: BaseEvent, options?: EventOptions): void { + const { serverModel, eventApiClient } = this.mediator; + + this?.logger?.verbose( + `${Messages.InformationMessages.StartingLogEvent}: ${event.name}` + ); + + if (this.store.canLog()) { + const uploadObject = serverModel.createEventObject(event); + eventApiClient.sendEventToServer(uploadObject, options); + } else { + this?.logger?.verbose(Messages.InformationMessages.AbandonLogEvent); + } + } + + logOptOut(): void { + this.logger.verbose(Messages.InformationMessages.StartingLogOptOut); + + // TODO: Should we Refactor to use logEvent or create a new wrapper for + // non-tracking events? + + const event = this.mediator.serverModel.createEventObject({ + // FIXME: Name is required + name: 'Opt Out Event', + messageType: MessageType.OptOut, + eventType: EventTypeEnum.Other, + }); + this.mediator.eventApiClient.sendEventToServer(event); + } + + logAST(): void { + this.logEvent({ + // FIXME: Name is required + name: 'Application State Transition', + messageType: MessageType.AppStateTransition, + }); + } + + logCommerceEvent( + commerceEvent: CommerceEvent, + attrs?: CustomAttributes, + options?: EventOptions + ): void { + this.logger.verbose( + Messages.InformationMessages.StartingLogCommerceEvent + ); + + // If a developer typos the ProductActionType, the event category will be + // null, resulting in kit forwarding errors on the server. + // The check for `ProductAction` is required to denote that these are + // ProductAction events, and not impression or promotions + if ( + commerceEvent.ProductAction && + commerceEvent.EventCategory === null + ) { + this.logger.error( + 'Commerce event not sent. The mParticle.ProductActionType you passed was invalid. Re-check your code.' + ); + return; + } + + attrs = this.mediator.mPInstance._Helpers.sanitizeAttributes( + attrs, + commerceEvent.EventName + ); + + if (this.store.canLog()) { + if (this.store.webviewBridgeEnabled) { + // Don't send shopping cart to parent sdks + commerceEvent.ShoppingCart = {}; + } + + if (attrs) { + commerceEvent.EventAttributes = attrs; + } + + this.mediator.eventApiClient.sendEventToServer( + commerceEvent, + options + ); + this.mediator.mPInstance._Persistence.update(); + } else { + this.logger.verbose(Messages.InformationMessages.AbandonLogEvent); + } + } + + // FIXME: Can we make this signature more consistent with the others? + logCheckoutEvent( + step: string, + option: EventOptions, // FIXME: Is this the correct type? + attrs: CustomAttributes, + customFlags: SDKEventCustomFlags + ): void { + // TODO: Define Commerce Event Object + const event = this.mediator.mPInstance.eCommerce.createCommerceEventObject( + customFlags + ); + + if (event) { + event.EventName += this.mediator.mPInstance.eCommerce.getProductActionEventName( + ProductActionType.Checkout + ); + event.EventCategory = CommerceEventType.ProductCheckout; + event.ProductAction = { + ProductActionType: ProductActionType.Checkout, + CheckoutStep: step, + CheckoutOptions: option, + ProductList: [], + }; + + this.logCommerceEvent(event, attrs); + } + } + + // FIXME: Can we make this signature more consistent with the others? + logProductActionEvent( + productActionType: ProductActionType, + product: Product, + customAttrs: CustomAttributes, + customFlags: SDKEventCustomFlags, + transactionAttributes: TransactionAttributes, + options?: EventOptions + ): void { + const { eCommerce } = this.mediator.mPInstance; + const event = eCommerce.createCommerceEventObject(customFlags); + + // FIXME: Update this once Product has the correct interface + const productList: IProduct[] = Array.isArray(product) + ? (product as IProduct[]) + : [product as IProduct]; + + productList.forEach(function (product) { + if (product.TotalAmount) { + product.TotalAmount = eCommerce.sanitizeAmount( + product.TotalAmount, + 'TotalAmount' + ); + } + if (product.Position) { + product.Position = eCommerce.sanitizeAmount( + product.Position, + 'Position' + ); + } + if (product.Price) { + product.Price = eCommerce.sanitizeAmount( + product.Price, + 'Price' + ); + } + if (product.Quantity) { + product.Quantity = eCommerce.sanitizeAmount( + product.Quantity, + 'Quantity' + ); + } + }); + + if (event) { + event.EventCategory = eCommerce.convertProductActionToEventType( + productActionType + ); + event.EventName += eCommerce.getProductActionEventName( + productActionType + ); + event.ProductAction = { + ProductActionType: productActionType, + ProductList: productList, + }; + + if (isObject(transactionAttributes)) { + eCommerce.convertTransactionAttributesToProductAction( + transactionAttributes, + event.ProductAction + ); + } + + this.logCommerceEvent(event, customAttrs, options); + } + } + + logPurchaseEvent( + transactionAttributes: TransactionAttributes, + product: Product, + attrs?: CustomAttributes, + customFlags?: SDKEventCustomFlags + ): void { + const { eCommerce } = this.mediator.mPInstance; + var event = eCommerce.createCommerceEventObject(customFlags); + + if (event) { + event.EventName += eCommerce.getProductActionEventName( + ProductActionType.Purchase + ); + event.EventCategory = CommerceEventType.ProductPurchase; + event.ProductAction = { + ProductActionType: ProductActionType.Purchase, + }; + event.ProductAction.ProductList = eCommerce.buildProductList( + event, + product + ); + + eCommerce.convertTransactionAttributesToProductAction( + transactionAttributes, + event.ProductAction + ); + + this.logCommerceEvent(event, attrs); + } + } + + logRefundEvent( + transactionAttributes: TransactionAttributes, + product: Product, + attrs?: CustomAttributes, + customFlags?: SDKEventCustomFlags + ): void { + const { eCommerce } = this.mediator.mPInstance; + + if (!transactionAttributes) { + this.logger.error(Messages.ErrorMessages.TransactionRequired); + return; + } + + const event = eCommerce.createCommerceEventObject(customFlags); + + if (event) { + event.EventName += eCommerce.getProductActionEventName( + ProductActionType.Refund + ); + event.EventCategory = CommerceEventType.ProductRefund; + event.ProductAction = { + ProductActionType: ProductActionType.Refund, + }; + event.ProductAction.ProductList = eCommerce.buildProductList( + event, + product + ); + + eCommerce.convertTransactionAttributesToProductAction( + transactionAttributes, + event.ProductAction + ); + + this.logCommerceEvent(event, attrs); + } + } + + logPromotionEvent( + promotionType: string, + promotion: SDKPromotion, + attrs?: CustomAttributes, + customFlags?: SDKEventCustomFlags, + eventOptions?: EventOptions + ): void { + const { _Ecommerce: eCommerce } = this.mediator.mPInstance; + + debugger; + + const event = eCommerce.createCommerceEventObject(customFlags); + + if (event) { + event.EventName += eCommerce.getPromotionActionEventName( + promotionType + ); + event.EventCategory = eCommerce.convertPromotionActionToEventType( + promotionType + ); + debugger; + event.PromotionAction = { + PromotionActionType: (promotionType as unknown) as string, + PromotionList: Array.isArray(promotion) + ? promotion + : [promotion], + }; + + this.logCommerceEvent(event, attrs, eventOptions); + } + } + + logImpressionEvent( + impression: Impression | Impression[], + attrs?: CustomAttributes, + customFlags?: SDKEventCustomFlags, + options?: EventOptions + ): void { + const event = this.mediator.mPInstance.eCommerce.createCommerceEventObject( + customFlags + ); + + if (event) { + event.EventName += 'Impression'; + event.EventCategory = CommerceEventType.ProductImpression; + + const impressionList: Impression[] = Array.isArray(impression) + ? impression + : [impression]; + + event.ProductImpressions = []; + + impressionList.forEach(function (impression) { + event.ProductImpressions.push({ + ProductImpressionList: impression.Name, + ProductList: Array.isArray(impression.Product) + ? impression.Product + : [impression.Product], + }); + }); + + this.logCommerceEvent(event, attrs, options); + } + } + + startTracking(callback: PositionCallback): void { + const triggerCallback: TrackingCallback = ( + callback: PositionCallback, + position?: GeolocationPosition + ) => { + if (callback) { + try { + if (position) { + callback(position); + } else { + // @ts-ignore: Executes callback as async function + callback(); + } + } catch (e) { + this.logger.error( + 'Error invoking the callback passed to startTrackingLocation.' + ); + this.logger.error(e as string); + } + } + }; + + const successTracking: PositionCallback = ( + position: GeolocationPosition + ) => { + this.store.currentPosition = { + lat: position.coords.latitude, + lng: position.coords.longitude, + }; + + triggerCallback(callback, position); + + // prevents callback from being fired multiple times + callback = null; + + this.store.isTracking = true; + }; + + const errorTracking: PositionErrorCallback = () => { + triggerCallback(callback); + // prevents callback from being fired multiple times + callback = null; + this.store.isTracking = false; + }; + + if (!this.store.isTracking) { + if ('geolocation' in navigator) { + this.store.watchPositionId = navigator.geolocation.watchPosition( + successTracking, + errorTracking + ); + } + } else { + const position: GeolocationPosition = { + // FIXME: Timestamp is required + timestamp: null, + coords: { + latitude: this.store.currentPosition.lat, + longitude: this.store.currentPosition.lng, + // FIXME: GeolocationCoordinates requires lat/long as numbers + } as GeolocationCoordinates, + }; + triggerCallback(callback, position); + } + } + + stopTracking(): void { + if (this.store.isTracking) { + navigator.geolocation.clearWatch(this.store.watchPositionId); + this.store.currentPosition = null; + this.store.isTracking = false; + } + } + + addEventHandler( + // QUESTION: are these the right types? + domEvent: keyof HTMLElementEventMap, + selector: HTMLElement, + eventName: string | ((string) => string), // FIXME: Why is this both a function or a string? + data: Dictionary | ((any) => Dictionary), // FIXME: Should this be a different type? + eventType: EventTypeEnum + ): void { + const self = this; + let elements: HTMLElement[]; + let handler = function (e) { + var timeoutHandler = function () { + if (element.href) { + window.location.href = element.href; + } else if (element.submit) { + element.submit(); + } + }; + + self.logger.verbose('DOM event triggered, handling event'); + + // FIXME: When would EventName or Data be a function? + self.logEvent({ + messageType: MessageType.PageEvent, + name: + typeof eventName === 'function' + ? eventName(element) + : eventName, + data: typeof data === 'function' ? data(element) : data, + eventType: eventType || EventTypeEnum.Other, + }); + + // TODO: Handle middle-clicks and special keys (ctrl, alt, etc) + if ( + (element.href && element.target !== '_blank') || + element.submit + ) { + // Give xmlhttprequest enough time to execute before navigating a link or submitting form + + if (e.preventDefault) { + e.preventDefault(); + } else { + e.returnValue = false; + } + + // @ts-ignore // FIXME: Should we add this to SDK Config? + setTimeout(timeoutHandler, self.store.SDKConfig.timeout); + } + }, + element, + i; + + if (!selector) { + this.logger.error("Can't bind event, selector is required"); + return; + } + + // Handle a css selector string or a dom element + if (typeof selector === 'string') { + elements = (document.querySelectorAll( + selector + ) as unknown) as HTMLInputElement[]; + } else if (selector.nodeType) { + elements = [selector]; + } + + if (elements.length) { + this.logger.verbose( + 'Found ' + + elements.length + + ' element' + + (elements.length > 1 ? 's' : '') + + ', attaching event handlers' + ); + + for (i = 0; i < elements.length; i++) { + element = elements[i]; + + if (element.addEventListener) { + element.addEventListener(domEvent, handler, false); + } else if (element.attachEvent) { + element.attachEvent('on' + domEvent, handler); + } else { + element['on' + domEvent] = handler; + } + } + } else { + this.logger.verbose('No elements found'); + } + } +} diff --git a/src/NextGen/Mediator.ts b/src/NextGen/Mediator.ts new file mode 100644 index 000000000..536cf0010 --- /dev/null +++ b/src/NextGen/Mediator.ts @@ -0,0 +1,48 @@ +import { Logger } from '@mparticle/web-sdk'; +import { IServerModel } from '../serverModel'; +import { IStore } from '../store'; +import { IEventLogging } from './EventLogging'; +import { MParticleWebSDK, SDKHelpersApi } from '../sdkRuntimeModels'; +import { IAPIClient } from '../apiClient'; + +export interface IMediator { + eventLogging: IEventLogging; + logger: Logger; + store: IStore; + + // TODO: Refactor Server Model to decouple mPInstance + serverModel?: IServerModel; +} + +export default class Mediator implements IMediator { + public eventLogging: IEventLogging; + public logger: Logger; + public store: IStore; + + // TODO: Decouple Helpers from mPInstance + public helpers: SDKHelpersApi; + + // TODO: These should components of Event Logging + public serverModel?: IServerModel; + public eventApiClient: IAPIClient; + + // TODO: Decouple this + public mPInstance: MParticleWebSDK; + + constructor( + eventLogging: IEventLogging, + store: IStore, + logger: Logger, + mPInstance: MParticleWebSDK + ) { + this.eventLogging = eventLogging; + this.logger = logger; + this.store = store; + + // Define convenience methods to facilitate mPInstance + // decoupling in the future + this.mPInstance = mPInstance; + this.helpers = this.mPInstance._Helpers; + // this.eventApiClient = this.mPInstance._APIClient; + } +} diff --git a/src/apiClient.ts b/src/apiClient.ts index 5f6207640..2633ffa3d 100644 --- a/src/apiClient.ts +++ b/src/apiClient.ts @@ -1,5 +1,5 @@ import Constants from './constants'; -import Types from './types'; +import { MessageType } from './types.interfaces'; import { BatchUploader } from './batchUploader'; import { MParticleUser, @@ -124,7 +124,7 @@ export default function APIClient( this.queueEventForBatchUpload(event); } - if (event.EventName !== Types.MessageType.AppStateTransition) { + if (event.EventName !== MessageType.AppStateTransition.toString()) { if (kitBlocker && kitBlocker.kitBlockingEnabled) { event = kitBlocker.createBlockedEvent(event); } diff --git a/src/batchUploader.ts b/src/batchUploader.ts index 2312c26ef..dd5db5583 100644 --- a/src/batchUploader.ts +++ b/src/batchUploader.ts @@ -7,9 +7,9 @@ import { SDKLoggerApi, } from './sdkRuntimeModels'; import { convertEvents } from './sdkToEventsApiConverter'; -import Types from './types'; import { getRampNumber, isEmpty } from './utils'; import { SessionStorageVault, LocalStorageVault } from './vault'; +import { TriggerUploadType } from './types.interfaces'; /** * BatchUploader contains all the logic to store/retrieve events and batches @@ -180,7 +180,7 @@ export class BatchUploader { // https://go.mparticle.com/work/SQDSDKS-3720 if ( !this.batchingEnabled || - Types.TriggerUploadType[event.EventDataType] + TriggerUploadType[event.EventDataType] ) { this.prepareAndUpload(false, false); } diff --git a/src/helpers.js b/src/helpers.js index daf03c724..a416d68df 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -8,18 +8,14 @@ var StorageNames = Constants.StorageNames; export default function Helpers(mpInstance) { var self = this; - this.canLog = function() { - if ( - mpInstance._Store.isEnabled && - (mpInstance._Store.devToken || - mpInstance._Store.webviewBridgeEnabled) - ) { - return true; - } - return false; + // TOOD: Update references to `canLog` using Store method + // QUESTION: Can we move this into Store? + this.canLog = function() { + return mpInstance._Store.canLog(); }; + // QUESTION: Can we move this into Store? this.getFeatureFlag = function(feature) { if (mpInstance._Store.SDKConfig.flags.hasOwnProperty(feature)) { return mpInstance._Store.SDKConfig.flags[feature]; diff --git a/src/mockBatchCreator.ts b/src/mockBatchCreator.ts index e292a1ea5..74eca93a3 100644 --- a/src/mockBatchCreator.ts +++ b/src/mockBatchCreator.ts @@ -34,6 +34,7 @@ export default class _BatchValidator { _APIClient: null, MPSideloadedKit: null, _Consent: null, + _Ecommerce: null, _Forwarders: null, _NativeSdkHelpers: null, _Persistence: null, diff --git a/src/mp-instance.js b/src/mp-instance.js index 149695b17..2dfd159dd 100644 --- a/src/mp-instance.js +++ b/src/mp-instance.js @@ -27,7 +27,6 @@ import Ecommerce from './ecommerce'; import Store from './store'; import Logger from './logger'; import Persistence from './persistence'; -import Events from './events'; import Forwarders from './forwarders'; import ServerModel from './serverModel'; import ForwardingStatsUploader from './forwardingStatsUploader'; @@ -37,6 +36,9 @@ import KitBlocker from './kitBlocking'; import ConfigAPIClient from './configAPIClient'; import IdentityAPIClient from './identityApiClient'; +import Mediator from './NextGen/Mediator'; +import EventLogging from './NextGen/EventLogging'; + var Messages = Constants.Messages, HTTPCodes = Constants.HTTPCodes; @@ -61,7 +63,10 @@ export default function mParticleInstance(instanceName) { this._SessionManager = new SessionManager(this); this._Persistence = new Persistence(this); this._Helpers = new Helpers(this); - this._Events = new Events(this); + + // TODO: Safely delete this + // this._Events = new Events(this); + this._CookieSyncManager = new CookieSyncManager(this); this._ServerModel = new ServerModel(this); this._Ecommerce = new Ecommerce(this); @@ -86,6 +91,14 @@ export default function mParticleInstance(instanceName) { this.generateHash = this._Helpers.generateHash; this.getDeviceId = this._Persistence.getDeviceId; + // QUESTION: Should we only init the mediator here or in init? + this.mediator = new Mediator(null, this._Store, this.Logger, this); + + // // QUESTION: will this cause some sort of cyclical dependency? + // const eventLogging = new EventLogging(this.mediator); + // this.mediator.eventLogging = eventLogging; + // this.mediator.serverModel = this._ServerModel; + if (typeof window !== 'undefined') { if (window.mParticle && window.mParticle.config) { if (window.mParticle.config.hasOwnProperty('rq')) { @@ -119,6 +132,37 @@ export default function mParticleInstance(instanceName) { } else { completeSDKInitialization(apiKey, config, this); } + + // // QUESTION: When should we load mediator? + // // In some cases, sdk init is "completed" a synchronousely, so we + // // may need to backfil mediator after this completes. + + // // Mediator acts as a conduit for the SDK core to send messages + // // back and forth between components, while allowing our code to be + // // modular + // // TODO: Maybe we make a separate initMediator function? + // this.mediator = new Mediator(null, this._Store, this.Logger, this); + + // // QUESTION: Should we add mediator to the instance? + + // const eventLogging = new EventLogging(this.mediator); + // this.mediator.eventLogging = eventLogging; + // this.mediator.serverModel = this._ServerModel; + + // debugger; + // // TODO: When should this be set? + // this.mediator.eventApiClient = this._APIClient; + + // debugger; + + // processReadyQueue(this); + + // debugger; + + // QUESTION: can we make this a store function? + // if (this._Store.isFirstRun) { + // this._Store.isFirstRun = false; + // } } else { console.error( 'No config available on the window, please pass a config object to mParticle.init()' @@ -152,7 +196,8 @@ export default function mParticleInstance(instanceName) { } }; - this._resetForTests = function(config, keepPersistence, instance) { + this._resetForTests = function (config, keepPersistence, instance) { + // QUESTION: Should we clear out mediator here? if (instance._Store) { delete instance._Store; } @@ -160,7 +205,10 @@ export default function mParticleInstance(instanceName) { instance._Store.isLocalStorageAvailable = instance._Persistence.determineLocalStorageAvailability( window.localStorage ); - instance._Events.stopTracking(); + + // FIXME: This breaks a before/each for several tests + // instance._Events.stopTracking(); + if (!keepPersistence) { instance._Persistence.resetPersistence(); } @@ -173,6 +221,12 @@ export default function mParticleInstance(instanceName) { forwarderConstructors: [], isDevelopmentMode: false, }; + + if (instance.mediator) { + delete instance.mediator; + } + + initializeMediator(instance); }; /** * A callback method that is invoked after mParticle is initialized. @@ -276,7 +330,7 @@ export default function mParticleInstance(instanceName) { */ this.stopTrackingLocation = function() { self._SessionManager.resetSessionTimer(); - self._Events.stopTracking(); + self.mediator.eventLogging.stopTracking(); }; /** * Starts tracking the location of the user @@ -291,7 +345,7 @@ export default function mParticleInstance(instanceName) { } self._SessionManager.resetSessionTimer(); - self._Events.startTracking(callback); + self.mediator.eventLogging.startTracking(callback); }; /** * Sets the position of the user @@ -361,7 +415,7 @@ export default function mParticleInstance(instanceName) { return; } - self._Events.logEvent(event, eventOptions); + self.mediator.eventLogging.logEvent(event, eventOptions); }; /** * Logs an event to mParticle's servers @@ -416,7 +470,7 @@ export default function mParticleInstance(instanceName) { return; } - self._Events.logEvent( + self.mediator.eventLogging.logEvent( { messageType: Types.MessageType.PageEvent, name: eventName, @@ -465,13 +519,16 @@ export default function mParticleInstance(instanceName) { } } - self._Events.logEvent({ + self.mediator.eventLogging.logEvent({ messageType: Types.MessageType.CrashReport, name: error.name ? error.name : 'Error', data: data, eventType: Types.EventType.Other, }); }; + // FIXME: Docs link is broke + // Should be: https://docs.mparticle.com/developers/sdk/web/event-tracking/#event-type + // https://go.mparticle.com/work/SQDSDKS-5676 /** * Logs `click` events * @method logLink @@ -481,7 +538,7 @@ export default function mParticleInstance(instanceName) { * @param {Object} [eventInfo] Attributes for the event */ this.logLink = function(selector, eventName, eventType, eventInfo) { - self._Events.addEventHandler( + self.mediator.eventLogging.addEventHandler( 'click', selector, eventName, @@ -498,7 +555,7 @@ export default function mParticleInstance(instanceName) { * @param {Object} [eventInfo] Attributes for the event */ this.logForm = function(selector, eventName, eventType, eventInfo) { - self._Events.addEventHandler( + self.mediator.eventLogging.addEventHandler( 'submit', selector, eventName, @@ -550,7 +607,7 @@ export default function mParticleInstance(instanceName) { } } - self._Events.logEvent( + self.mediator.eventLogging.logEvent( { messageType: Types.MessageType.PageView, name: eventName, @@ -829,7 +886,12 @@ export default function mParticleInstance(instanceName) { } self._SessionManager.resetSessionTimer(); - self._Events.logCheckoutEvent(step, option, attrs, customFlags); + self.mediator.eventLogging.logCheckoutEvent( + step, + option, + attrs, + customFlags + ); }, /** * Logs a product action @@ -864,7 +926,7 @@ export default function mParticleInstance(instanceName) { if (queued) return; self._SessionManager.resetSessionTimer(); - self._Events.logProductActionEvent( + self.mediator.eventLogging.logProductActionEvent( productActionType, product, attrs, @@ -911,7 +973,7 @@ export default function mParticleInstance(instanceName) { return; } self._SessionManager.resetSessionTimer(); - self._Events.logPurchaseEvent( + self.mediator.eventLogging.logPurchaseEvent( transactionAttributes, product, attrs, @@ -978,7 +1040,7 @@ export default function mParticleInstance(instanceName) { if (queued) return; self._SessionManager.resetSessionTimer(); - self._Events.logImpressionEvent( + self.mediator.eventLogging.logImpressionEvent( impression, attrs, customFlags, @@ -1019,7 +1081,7 @@ export default function mParticleInstance(instanceName) { return; } self._SessionManager.resetSessionTimer(); - self._Events.logRefundEvent( + self.mediator.eventLogging.logRefundEvent( transactionAttributes, product, attrs, @@ -1097,7 +1159,7 @@ export default function mParticleInstance(instanceName) { self._SessionManager.resetSessionTimer(); self._Store.isEnabled = !isOptingOut; - self._Events.logOptOut(); + self.mediator.eventLogging.logOptOut(); self._Persistence.update(); if (self._Store.activeForwarders.length) { @@ -1285,7 +1347,24 @@ export default function mParticleInstance(instanceName) { function completeSDKInitialization(apiKey, config, mpInstance) { var kitBlocker = createKitBlocker(config, mpInstance); + // mpInstance.mediator = new Mediator( + // null, + // mpInstance._Store, + // mpInstance.Logger, + // mpInstance + // ); + + // QUESTION: When should we do this? mpInstance._APIClient = new APIClient(mpInstance, kitBlocker); + + // TODO: Can we move this elsewhere? + mpInstance.mediator.eventApiClient = mpInstance._APIClient; + + const eventLogging = new EventLogging(mpInstance.mediator); + + mpInstance.mediator.eventLogging = eventLogging; + mpInstance.mediator.serverModel = mpInstance._ServerModel; + mpInstance._Forwarders = new Forwarders(mpInstance, kitBlocker); if (config.flags) { if ( @@ -1397,8 +1476,13 @@ function completeSDKInitialization(apiKey, config, mpInstance) { mpInstance._Forwarders.processPixelConfigs(config); + // debugger; mpInstance._SessionManager.initialize(); - mpInstance._Events.logAST(); + + // debugger; + // FIXME: Needs to use mediator but might have to fire before mediator? + // mpInstance._Events.logAST(); + mpInstance.mediator.eventLogging.logAST(); // Call mParticle._Store.SDKConfig.identityCallback when identify was not called due to a reload or a sessionId already existing if ( @@ -1438,28 +1522,71 @@ function completeSDKInitialization(apiKey, config, mpInstance) { } } + // debugger; + mpInstance._Store.isInitialized = true; - // Call any functions that are waiting for the library to be initialized - if ( - mpInstance._preInit.readyQueue && - mpInstance._preInit.readyQueue.length - ) { - mpInstance._preInit.readyQueue.forEach(function(readyQueueItem) { - if (mpInstance._Helpers.Validators.isFunction(readyQueueItem)) { - readyQueueItem(); - } else if (Array.isArray(readyQueueItem)) { - processPreloadedItem(readyQueueItem, mpInstance); - } - }); - mpInstance._preInit.readyQueue = []; - } + // debugger; + + // initializeMediator(mpInstance); + + // FIXME: Is this safe? + // mpInstance.mediator.eventLogging.logAST(); + // TODO: why are we doing this twice? + processReadyQueue(mpInstance); + + // Call any functions that are waiting for the library to be initialized + // if ( + // mpInstance._preInit.readyQueue && + // mpInstance._preInit.readyQueue.length + // ) { + // mpInstance._preInit.readyQueue.forEach(function(readyQueueItem) { + // if (mpInstance._Helpers.Validators.isFunction(readyQueueItem)) { + // readyQueueItem(); + // } else if (Array.isArray(readyQueueItem)) { + // processPreloadedItem(readyQueueItem, mpInstance); + // } + // }); + + // mpInstance._preInit.readyQueue = []; + // } + + // QUESTION: Can we move this into the store? if (mpInstance._Store.isFirstRun) { mpInstance._Store.isFirstRun = false; } } +// TODO: We should have this return a mediator, rather than take it as a param +function initializeMediator(mpInstance) { + // QUESTION: When should we load mediator? + // In some cases, sdk init is "completed" a synchronousely, so we + // may need to backfil mediator after this completes. + + // Mediator acts as a conduit for the SDK core to send messages + // back and forth between components, while allowing our code to be + // modular + // TODO: Maybe we make a separate initMediator function? + mpInstance.mediator = new Mediator( + null, + mpInstance._Store, + mpInstance.Logger, + + // TODO: Can we remove the need for mpInstance? + mpInstance + ); + const eventLogging = new EventLogging(mpInstance.mediator); + + mpInstance.mediator.eventLogging = eventLogging; + mpInstance.mediator.serverModel = mpInstance._ServerModel; + + // debugger; + + // TODO: When should this be set? + mpInstance.mediator.eventApiClient = mpInstance._APIClient; +} + function createKitBlocker(config, mpInstance) { var kitBlocker, dataPlanForKitBlocker, kitBlockError, kitBlockOptions; @@ -1524,6 +1651,14 @@ function createKitBlocker(config, mpInstance) { function runPreConfigFetchInitialization(mpInstance, apiKey, config) { mpInstance.Logger = new Logger(config); mpInstance._Store = new Store(config, mpInstance); + + // debugger; + + // TODO: Maybe create setters/getters? + // TODO: Maybe move this to after completeinit? + mpInstance.mediator.logger = mpInstance.Logger.logger; + mpInstance.mediator.store = mpInstance._Store; + window.mParticle.Store = mpInstance._Store; mpInstance._Store.devToken = apiKey || null; mpInstance.Logger.verbose( @@ -1569,6 +1704,24 @@ function processPreloadedItem(readyQueueItem, mpInstance) { } } +function processReadyQueue(mpInstance) { + // debugger; + if ( + mpInstance._preInit.readyQueue && + mpInstance._preInit.readyQueue.length + ) { + mpInstance._preInit.readyQueue.forEach(function (readyQueueItem) { + if (mpInstance._Helpers.Validators.isFunction(readyQueueItem)) { + readyQueueItem(); + } else if (Array.isArray(readyQueueItem)) { + processPreloadedItem(readyQueueItem, mpInstance); + } + }); + + mpInstance._preInit.readyQueue = []; + } +} + function queueIfNotInitialized(func, self) { if (!self.isInitialized()) { self.ready(function() { diff --git a/src/sdkRuntimeModels.ts b/src/sdkRuntimeModels.ts index 69a9a6f37..3abf38d82 100644 --- a/src/sdkRuntimeModels.ts +++ b/src/sdkRuntimeModels.ts @@ -1,6 +1,10 @@ import * as EventsApi from '@mparticle/event-models'; import { DataPlanVersion } from '@mparticle/data-planning-models'; -import { MPConfiguration, IdentityApiData } from '@mparticle/web-sdk'; +import { + MPConfiguration, + IdentityApiData, + TransactionAttributes, +} from '@mparticle/web-sdk'; import { IStore } from './store'; import Validators from './validators'; import { Dictionary } from './utils'; @@ -9,6 +13,11 @@ import { IKitConfigs } from './configAPIClient'; import { SDKConsentApi, SDKConsentState } from './consent'; import { IPersistence } from './persistence.interfaces'; import { IMPSideloadedKit } from './sideloadedKit'; +import { + CommerceEventType, + ProductActionType, + PromotionActionType, +} from './types.interfaces'; // TODO: Resolve this with version in @mparticle/web-sdk export type SDKEventCustomFlags = Dictionary; @@ -133,6 +142,7 @@ export interface MParticleWebSDK { MPSideloadedKit: IMPSideloadedKit; _APIClient: any; // TODO: Set up API Client _Store: IStore; + _Ecommerce: SDKEcommerceAPI; _Forwarders: any; _Helpers: SDKHelpersApi; config: SDKInitConfig; @@ -230,7 +240,26 @@ export interface SDKIdentityApi { modify; } +export interface SDKEcommerceAPI { + convertTransactionAttributesToProductAction?( + transactionAttributes: TransactionAttributes, + productAction: SDKProductAction + ): void; + + // TODO: Refactor this to be a type of ProductAction + getProductActionEventName?(productActionType: string): string; + getPromotionActionEventName?(promotionActionType: string): string; + convertProductActionToEventType?( + productActionType: string + ): CommerceEventType | null; + convertPromotionActionToEventType?( + promotionActionType: string + ): PromotionActionType | null; + createCommerceEventObject?(customFlags: SDKEventCustomFlags): SDKEvent; +} + export interface SDKHelpersApi { + canLog?(): boolean; createServiceUrl(arg0: string, arg1: string): void; createXHR?(cb: () => void): XMLHttpRequest; extend?(...args: any[]); diff --git a/src/sessionManager.js b/src/sessionManager.js index 048284047..6f70d898f 100644 --- a/src/sessionManager.js +++ b/src/sessionManager.js @@ -5,7 +5,7 @@ var Messages = Constants.Messages; function SessionManager(mpInstance) { var self = this; - this.initialize = function() { + this.initialize = function () { if (mpInstance._Store.sessionId) { var sessionTimeoutInMilliseconds = mpInstance._Store.SDKConfig.sessionTimeout * 60000; @@ -35,11 +35,11 @@ function SessionManager(mpInstance) { } }; - this.getSession = function() { + this.getSession = function () { return mpInstance._Store.sessionId; }; - this.startNewSession = function() { + this.startNewSession = function () { mpInstance.Logger.verbose( Messages.InformationMessages.StartingNewSession ); @@ -71,7 +71,7 @@ function SessionManager(mpInstance) { mpInstance._Store.SDKConfig.identityCallback = null; } - mpInstance._Events.logEvent({ + mpInstance.mediator.eventLogging.logEvent({ messageType: Types.MessageType.SessionStart, }); } else { @@ -81,13 +81,13 @@ function SessionManager(mpInstance) { } }; - this.endSession = function(override) { + this.endSession = function (override) { mpInstance.Logger.verbose( Messages.InformationMessages.StartingEndSession ); if (override) { - mpInstance._Events.logEvent({ + mpInstance.mediator.eventLogging.logEvent({ messageType: Types.MessageType.SessionEnd, }); @@ -128,7 +128,7 @@ function SessionManager(mpInstance) { if (timeSinceLastEventSent < sessionTimeoutInMilliseconds) { self.setSessionTimer(); } else { - mpInstance._Events.logEvent({ + mpInstance.mediator.eventLogging.logEvent({ messageType: Types.MessageType.SessionEnd, }); @@ -146,16 +146,16 @@ function SessionManager(mpInstance) { } }; - this.setSessionTimer = function() { + this.setSessionTimer = function () { var sessionTimeoutInMilliseconds = mpInstance._Store.SDKConfig.sessionTimeout * 60000; - mpInstance._Store.globalTimer = window.setTimeout(function() { + mpInstance._Store.globalTimer = window.setTimeout(function () { self.endSession(); }, sessionTimeoutInMilliseconds); }; - this.resetSessionTimer = function() { + this.resetSessionTimer = function () { if (!mpInstance._Store.webviewBridgeEnabled) { if (!mpInstance._Store.sessionId) { self.startNewSession(); @@ -166,11 +166,11 @@ function SessionManager(mpInstance) { self.startNewSessionIfNeeded(); }; - this.clearSessionTimeout = function() { + this.clearSessionTimeout = function () { clearTimeout(mpInstance._Store.globalTimer); }; - this.startNewSessionIfNeeded = function() { + this.startNewSessionIfNeeded = function () { if (!mpInstance._Store.webviewBridgeEnabled) { var persistence = mpInstance._Persistence.getPersistence(); diff --git a/src/store.ts b/src/store.ts index 2ee1d91d8..3b16fb8d9 100644 --- a/src/store.ts +++ b/src/store.ts @@ -154,6 +154,8 @@ export interface IStore { integrationDelayTimeoutStart: number; // UNIX Timestamp webviewBridgeEnabled?: boolean; wrapperSDKInfo: WrapperSDKInfo; + + canLog?(): boolean; } // TODO: Merge this with SDKStoreApi in sdkRuntimeModels @@ -441,4 +443,8 @@ export default function Store( this.SDKConfig.flags[Constants.FeatureFlags.OfflineStorage] = 0; } } + + this.canLog = (): boolean => + this.isEnabled && + ((this.devToken || this.webviewBridgeEnabled) as boolean); } diff --git a/src/types.interfaces.ts b/src/types.interfaces.ts index 3339a8970..d11f4ac23 100644 --- a/src/types.interfaces.ts +++ b/src/types.interfaces.ts @@ -25,7 +25,7 @@ export enum MessageType { UserAttributeChange = 17, UserIdentityChange = 18, Media = 20, -}; +} export enum IdentityType { Other = 0, @@ -49,4 +49,46 @@ export enum IdentityType { MobileNumber = 19, PhoneNumber2 = 20, PhoneNumber3 = 21, -} \ No newline at end of file +} + +export enum CommerceEventType { + ProductAddToCart = 10, + ProductRemoveFromCart = 11, + ProductCheckout = 12, + ProductCheckoutOption = 13, + ProductClick = 14, + ProductViewDetail = 15, + ProductPurchase = 16, + ProductRefund = 17, + PromotionView = 18, + PromotionClick = 19, + ProductAddToWishlist = 20, + ProductRemoveFromWishlist = 21, + ProductImpression = 22, +} + +export enum ProductActionType { + Unknown = 0, + AddToCart = 1, + RemoveFromCart = 2, + Checkout = 3, + CheckoutOption = 4, + Click = 5, + ViewDetail = 6, + Purchase = 7, + Refund = 8, + AddToWishlist = 9, + RemoveFromWishlist = 10, +} + +export enum PromotionActionType { + Unknown = 0, + PromotionView = 1, + PromotionClick = 2, +} + +export const TriggerUploadType = { + [MessageType.Commerce]: 1, + [MessageType.UserAttributeChange]: 1, + [MessageType.UserIdentityChange]: 1, +}; diff --git a/src/types.js b/src/types.js index 822478e6f..3dd974b56 100644 --- a/src/types.js +++ b/src/types.js @@ -21,6 +21,8 @@ var TriggerUploadType = { [MessageType.UserIdentityChange]: 1, }; +// FIXME: Break up Enums and Reverse Lookup Objects +// https://go.mparticle.com/work/SQDSDKS-5677 var EventType = { Unknown: 0, Navigation: 1, diff --git a/test/jest/NextGen/MPEventLogging.spec.ts b/test/jest/NextGen/MPEventLogging.spec.ts new file mode 100644 index 000000000..a9efc5a45 --- /dev/null +++ b/test/jest/NextGen/MPEventLogging.spec.ts @@ -0,0 +1,10 @@ +import Mediator from '../../../src/NextGen/Mediator'; +import { IEventLogging } from '../../../src/NextGen/EventLogging'; + +describe('Next Gen', () => { + describe('EventLogging', () => { + describe('constructor', () => { + it('initializes with event logging', () => {}); + }); + }); +}); diff --git a/test/jest/NextGen/MPMediator.spec.ts b/test/jest/NextGen/MPMediator.spec.ts new file mode 100644 index 000000000..de3629326 --- /dev/null +++ b/test/jest/NextGen/MPMediator.spec.ts @@ -0,0 +1,76 @@ +import Mediator from '../../../src/NextGen/Mediator'; +import { IEventLogging } from '../../../src/NextGen/EventLogging'; +import { IStore } from '../../../src/store'; +import { Logger } from '@mparticle/web-sdk'; +import { MParticleWebSDK } from '../../../src/sdkRuntimeModels'; +import { MessageType, EventTypeEnum } from '../../../src/types.interfaces'; +import APIClient, { IAPIClient } from '../../../src/apiClient'; + +describe('Next Gen', () => { + describe('Mediator', () => { + describe('constructor', () => { + it('initializes with event logging', () => { + const logEventSpy = jest.fn(); + const eventLogger: IEventLogging = { + logEvent: logEventSpy, + }; + + const mockStore: IStore = ({} as unknown) as IStore; + const mockLogger: Logger = ({} as unknown) as Logger; + const mockMPInstance: MParticleWebSDK = ({} as unknown) as MParticleWebSDK; + const mediator = new Mediator( + eventLogger, + mockStore, + mockLogger, + mockMPInstance + ); + + expect(mediator.eventLogging).toBeDefined(); + expect(mediator.eventLogging.logEvent).toBeDefined(); + }); + }); + + describe('#eventLogging', () => { + it('should send events to eventApiClient', () => { + const logEventSpy = jest.fn(); + const eventLogger: IEventLogging = { + logEvent: logEventSpy, + }; + + const mockMPInstance: MParticleWebSDK = ({} as unknown) as MParticleWebSDK; + + const mockEventApiClient: IAPIClient = new APIClient( + mockMPInstance, + null + ); + + const mockStore: IStore = ({} as unknown) as IStore; + const mockLogger: Logger = ({} as unknown) as Logger; + const mediator = new Mediator( + eventLogger, + mockStore, + mockLogger, + mockMPInstance + ); + + expect(mediator.eventLogging).toBeDefined(); + + mediator.eventLogging.logEvent({ + name: 'Test Event', + messageType: MessageType.PageEvent, + eventType: EventTypeEnum.Navigation, + data: { mykey: 'myvalue' }, + customFlags: {}, + }); + + expect(logEventSpy).toHaveBeenCalledWith({ + name: 'Test Event', + messageType: MessageType.PageEvent, + eventType: EventTypeEnum.Navigation, + data: { mykey: 'myvalue' }, + customFlags: {}, + }); + }); + }); + }); +});