From f678da38172be915d754f2317f00f1c3782f6643 Mon Sep 17 00:00:00 2001 From: Thijs Daniels Date: Wed, 27 Mar 2024 14:56:25 +0100 Subject: [PATCH] feat(react-tracking): experimental release --- .changeset/wild-terms-unite.md | 5 + package-lock.json | 217 ++++++++------- packages/react-tracking/CHANGELOG.md | 39 +++ packages/react-tracking/LICENSE.md | 9 + packages/react-tracking/README.md | 1 + .../contexts/trackingContext.ts | 112 ++++++++ .../hooks/useIntersectionTracker.ts | 68 +++++ .../react-tracking/hooks/useLoadTracker.ts | 24 ++ .../react-tracking/hooks/useMediaTracker.ts | 103 +++++++ packages/react-tracking/hooks/useTracker.ts | 7 + packages/react-tracking/index.ts | 8 + packages/react-tracking/package.json | 46 ++++ .../providers/TrackingProvider.tsx | 255 ++++++++++++++++++ packages/react-tracking/tsconfig.json | 5 + .../utilities/createConsoleTracker.ts | 9 + .../utilities/createGtmTracker.ts | 18 ++ .../utilities/createStorybookTracker.ts | 17 ++ 17 files changed, 850 insertions(+), 93 deletions(-) create mode 100644 .changeset/wild-terms-unite.md create mode 100644 packages/react-tracking/CHANGELOG.md create mode 100644 packages/react-tracking/LICENSE.md create mode 100644 packages/react-tracking/README.md create mode 100644 packages/react-tracking/contexts/trackingContext.ts create mode 100644 packages/react-tracking/hooks/useIntersectionTracker.ts create mode 100644 packages/react-tracking/hooks/useLoadTracker.ts create mode 100644 packages/react-tracking/hooks/useMediaTracker.ts create mode 100644 packages/react-tracking/hooks/useTracker.ts create mode 100644 packages/react-tracking/index.ts create mode 100644 packages/react-tracking/package.json create mode 100644 packages/react-tracking/providers/TrackingProvider.tsx create mode 100644 packages/react-tracking/tsconfig.json create mode 100644 packages/react-tracking/utilities/createConsoleTracker.ts create mode 100644 packages/react-tracking/utilities/createGtmTracker.ts create mode 100644 packages/react-tracking/utilities/createStorybookTracker.ts diff --git a/.changeset/wild-terms-unite.md b/.changeset/wild-terms-unite.md new file mode 100644 index 00000000..0c3691dd --- /dev/null +++ b/.changeset/wild-terms-unite.md @@ -0,0 +1,5 @@ +--- +"@codedazur/react-tracking": minor +--- + +The experimental release of the TrackingProvider and associated hooks and utilities. diff --git a/package-lock.json b/package-lock.json index 8a4a8bcb..67d027dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1175,7 +1175,7 @@ }, "node_modules/@babel/helper-string-parser": { "version": "7.22.5", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1240,7 +1240,7 @@ "version": "7.23.0", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", - "dev": true, + "devOptional": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -2818,7 +2818,7 @@ "version": "7.23.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", - "dev": true, + "devOptional": true, "dependencies": { "@babel/helper-string-parser": "^7.22.5", "@babel/helper-validator-identifier": "^7.22.20", @@ -3203,6 +3203,10 @@ "resolved": "packages/react-preferences", "link": true }, + "node_modules/@codedazur/react-tracking": { + "resolved": "packages/react-tracking", + "link": true + }, "node_modules/@codedazur/tsconfig": { "resolved": "packages/tsconfig", "link": true @@ -3266,7 +3270,7 @@ }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.1", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "react": ">=16.8.0" @@ -3707,7 +3711,7 @@ }, "node_modules/@floating-ui/core": { "version": "1.4.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.1.1" @@ -3715,7 +3719,7 @@ }, "node_modules/@floating-ui/dom": { "version": "1.5.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@floating-ui/core": "^1.4.1", @@ -3724,7 +3728,7 @@ }, "node_modules/@floating-ui/react-dom": { "version": "2.0.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.3.0" @@ -3736,7 +3740,7 @@ }, "node_modules/@floating-ui/utils": { "version": "0.1.1", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@humanwhocodes/config-array": { @@ -4587,7 +4591,7 @@ }, "node_modules/@juggle/resize-observer": { "version": "3.4.0", - "dev": true, + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@manypkg/find-root": { @@ -5007,7 +5011,7 @@ }, "node_modules/@radix-ui/number": { "version": "1.0.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10" @@ -5015,7 +5019,7 @@ }, "node_modules/@radix-ui/primitive": { "version": "1.0.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10" @@ -5023,7 +5027,7 @@ }, "node_modules/@radix-ui/react-arrow": { "version": "1.0.3", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -5046,7 +5050,7 @@ }, "node_modules/@radix-ui/react-collection": { "version": "1.0.3", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -5072,7 +5076,7 @@ }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.0.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10" @@ -5089,7 +5093,7 @@ }, "node_modules/@radix-ui/react-context": { "version": "1.0.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10" @@ -5106,7 +5110,7 @@ }, "node_modules/@radix-ui/react-direction": { "version": "1.0.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10" @@ -5123,7 +5127,7 @@ }, "node_modules/@radix-ui/react-dismissable-layer": { "version": "1.0.4", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -5150,7 +5154,7 @@ }, "node_modules/@radix-ui/react-focus-guards": { "version": "1.0.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10" @@ -5167,7 +5171,7 @@ }, "node_modules/@radix-ui/react-focus-scope": { "version": "1.0.3", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -5192,7 +5196,7 @@ }, "node_modules/@radix-ui/react-id": { "version": "1.0.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -5210,7 +5214,7 @@ }, "node_modules/@radix-ui/react-popper": { "version": "1.1.2", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -5242,7 +5246,7 @@ }, "node_modules/@radix-ui/react-portal": { "version": "1.0.3", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -5265,7 +5269,7 @@ }, "node_modules/@radix-ui/react-primitive": { "version": "1.0.3", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -5288,7 +5292,7 @@ }, "node_modules/@radix-ui/react-select": { "version": "1.2.2", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -5331,7 +5335,7 @@ }, "node_modules/@radix-ui/react-slot": { "version": "1.0.2", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -5349,7 +5353,7 @@ }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.0.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10" @@ -5366,7 +5370,7 @@ }, "node_modules/@radix-ui/react-use-controllable-state": { "version": "1.0.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -5384,7 +5388,7 @@ }, "node_modules/@radix-ui/react-use-escape-keydown": { "version": "1.0.3", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -5402,7 +5406,7 @@ }, "node_modules/@radix-ui/react-use-layout-effect": { "version": "1.0.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10" @@ -5419,7 +5423,7 @@ }, "node_modules/@radix-ui/react-use-previous": { "version": "1.0.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10" @@ -5436,7 +5440,7 @@ }, "node_modules/@radix-ui/react-use-rect": { "version": "1.0.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -5454,7 +5458,7 @@ }, "node_modules/@radix-ui/react-use-size": { "version": "1.0.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -5472,7 +5476,7 @@ }, "node_modules/@radix-ui/react-visually-hidden": { "version": "1.0.3", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -5495,7 +5499,7 @@ }, "node_modules/@radix-ui/rect": { "version": "1.0.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10" @@ -6071,7 +6075,7 @@ }, "node_modules/@storybook/addon-actions": { "version": "7.2.0", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@storybook/client-logger": "7.2.0", @@ -6110,7 +6114,7 @@ }, "node_modules/@storybook/addon-actions/node_modules/uuid": { "version": "9.0.0", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "uuid": "dist/bin/uuid" @@ -6931,7 +6935,7 @@ }, "node_modules/@storybook/channels": { "version": "7.2.0", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@storybook/channels": "7.2.0", @@ -7203,7 +7207,7 @@ }, "node_modules/@storybook/client-logger": { "version": "7.2.0", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0" @@ -7254,7 +7258,7 @@ }, "node_modules/@storybook/components": { "version": "7.2.0", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@radix-ui/react-select": "^1.2.2", @@ -7609,7 +7613,7 @@ }, "node_modules/@storybook/core-events": { "version": "7.2.0", - "dev": true, + "devOptional": true, "license": "MIT", "funding": { "type": "opencollective", @@ -7831,7 +7835,7 @@ }, "node_modules/@storybook/csf": { "version": "0.1.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "type-fest": "^2.19.0" @@ -7919,7 +7923,7 @@ }, "node_modules/@storybook/csf/node_modules/type-fest": { "version": "2.19.0", - "dev": true, + "devOptional": true, "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=12.20" @@ -7959,7 +7963,7 @@ }, "node_modules/@storybook/global": { "version": "5.0.0", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@storybook/instrumenter": { @@ -8174,7 +8178,7 @@ }, "node_modules/@storybook/manager-api": { "version": "7.2.0", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@storybook/channels": "7.2.0", @@ -8316,7 +8320,7 @@ }, "node_modules/@storybook/preview-api": { "version": "7.2.0", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@storybook/channels": "7.2.0", @@ -8516,7 +8520,7 @@ }, "node_modules/@storybook/router": { "version": "7.2.0", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@storybook/client-logger": "7.2.0", @@ -8672,7 +8676,7 @@ }, "node_modules/@storybook/theming": { "version": "7.2.0", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", @@ -8691,7 +8695,7 @@ }, "node_modules/@storybook/types": { "version": "7.2.0", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@storybook/channels": "7.2.0", @@ -8990,7 +8994,7 @@ }, "node_modules/@types/babel__core": { "version": "7.20.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.20.7", @@ -9002,7 +9006,7 @@ }, "node_modules/@types/babel__generator": { "version": "7.6.4", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" @@ -9010,7 +9014,7 @@ }, "node_modules/@types/babel__template": { "version": "7.4.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", @@ -9019,7 +9023,7 @@ }, "node_modules/@types/babel__traverse": { "version": "7.20.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/types": "^7.20.7" @@ -9027,7 +9031,7 @@ }, "node_modules/@types/body-parser": { "version": "1.19.2", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -9070,7 +9074,7 @@ }, "node_modules/@types/connect": { "version": "3.4.35", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -9134,7 +9138,7 @@ }, "node_modules/@types/express": { "version": "4.17.17", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -9145,7 +9149,7 @@ }, "node_modules/@types/express-serve-static-core": { "version": "4.17.35", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -9191,7 +9195,7 @@ }, "node_modules/@types/http-errors": { "version": "2.0.1", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/is-ci": { @@ -9249,7 +9253,7 @@ }, "node_modules/@types/mime": { "version": "1.3.2", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/mime-types": { @@ -9301,12 +9305,12 @@ }, "node_modules/@types/qs": { "version": "6.9.7", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.4", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { @@ -9337,7 +9341,7 @@ }, "node_modules/@types/send": { "version": "0.17.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/mime": "^1", @@ -9346,7 +9350,7 @@ }, "node_modules/@types/serve-static": { "version": "1.15.2", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -10067,7 +10071,7 @@ }, "node_modules/aria-hidden": { "version": "1.2.3", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -12792,7 +12796,7 @@ }, "node_modules/detect-node-es": { "version": "1.1.0", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/detect-package-manager": { @@ -14863,7 +14867,7 @@ }, "node_modules/file-system-cache": { "version": "2.3.0", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "fs-extra": "11.1.1", @@ -14872,7 +14876,7 @@ }, "node_modules/file-system-cache/node_modules/fs-extra": { "version": "11.1.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -14885,7 +14889,7 @@ }, "node_modules/file-system-cache/node_modules/jsonfile": { "version": "6.1.0", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "universalify": "^2.0.0" @@ -14896,7 +14900,7 @@ }, "node_modules/file-system-cache/node_modules/universalify": { "version": "2.0.0", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 10.0.0" @@ -15377,7 +15381,7 @@ }, "node_modules/get-nonce": { "version": "1.0.1", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -16036,7 +16040,7 @@ }, "node_modules/invariant": { "version": "2.2.4", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "loose-envify": "^1.0.0" @@ -18972,7 +18976,7 @@ }, "node_modules/map-or-similar": { "version": "1.5.0", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/markdown-to-jsx": { @@ -19028,7 +19032,7 @@ }, "node_modules/memoizerific": { "version": "1.11.3", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "map-or-similar": "^1.5.0" @@ -20024,7 +20028,7 @@ }, "node_modules/polished": { "version": "4.2.2", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.17.8" @@ -20497,7 +20501,7 @@ }, "node_modules/qs": { "version": "6.11.2", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.0.4" @@ -20542,7 +20546,7 @@ }, "node_modules/ramda": { "version": "0.29.0", - "dev": true, + "devOptional": true, "license": "MIT", "funding": { "type": "opencollective", @@ -20666,7 +20670,7 @@ }, "node_modules/react-inspector": { "version": "6.0.2", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "react": "^16.8.4 || ^17.0.0 || ^18.0.0" @@ -20691,7 +20695,7 @@ }, "node_modules/react-remove-scroll": { "version": "2.5.5", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "react-remove-scroll-bar": "^2.3.3", @@ -20715,7 +20719,7 @@ }, "node_modules/react-remove-scroll-bar": { "version": "2.3.4", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "react-style-singleton": "^2.2.1", @@ -20736,7 +20740,7 @@ }, "node_modules/react-style-singleton": { "version": "2.2.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "get-nonce": "^1.0.0", @@ -21840,7 +21844,7 @@ }, "node_modules/store2": { "version": "2.14.2", - "dev": true, + "devOptional": true, "license": "(MIT OR GPL-3.0)" }, "node_modules/storybook": { @@ -22243,7 +22247,7 @@ }, "node_modules/synchronous-promise": { "version": "2.0.17", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause" }, "node_modules/synckit": { @@ -22332,7 +22336,7 @@ }, "node_modules/telejson": { "version": "7.1.0", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "memoizerific": "^1.11.3" @@ -22586,7 +22590,7 @@ }, "node_modules/tiny-invariant": { "version": "1.3.1", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/tinybench": { @@ -22638,7 +22642,7 @@ }, "node_modules/to-fast-properties": { "version": "2.0.0", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=4" @@ -22712,7 +22716,7 @@ }, "node_modules/ts-dedent": { "version": "2.2.0", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.10" @@ -23512,7 +23516,7 @@ }, "node_modules/use-callback-ref": { "version": "1.3.0", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -23532,7 +23536,7 @@ }, "node_modules/use-resize-observer": { "version": "9.1.0", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@juggle/resize-observer": "^3.3.1" @@ -23544,7 +23548,7 @@ }, "node_modules/use-sidecar": { "version": "1.1.2", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "detect-node-es": "^1.1.0", @@ -23577,7 +23581,7 @@ }, "node_modules/util-deprecate": { "version": "1.0.2", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/utila": { @@ -25780,7 +25784,7 @@ }, "packages/essentials": { "name": "@codedazur/essentials", - "version": "1.6.1", + "version": "1.7.0", "license": "MIT", "devDependencies": { "@codedazur/eslint-config": "*", @@ -25827,7 +25831,7 @@ }, "packages/react-essentials": { "name": "@codedazur/react-essentials", - "version": "0.3.0", + "version": "1.1.1", "license": "MIT", "dependencies": { "@codedazur/essentials": "*" @@ -25871,7 +25875,7 @@ }, "packages/react-media": { "name": "@codedazur/react-media", - "version": "0.0.3", + "version": "1.0.0", "license": "MIT", "dependencies": { "@codedazur/essentials": "*", @@ -25892,7 +25896,7 @@ }, "packages/react-notifications": { "name": "@codedazur/react-notifications", - "version": "0.1.0", + "version": "0.1.3", "license": "MIT", "dependencies": { "@codedazur/essentials": "*", @@ -25969,6 +25973,33 @@ "react": ">=16.8.0" } }, + "packages/react-tracking": { + "name": "@codedazur/react-tracking", + "version": "0.0.0", + "license": "MIT", + "dependencies": { + "@codedazur/essentials": "*", + "@codedazur/react-essentials": "*" + }, + "devDependencies": { + "@codedazur/eslint-config": "*", + "@codedazur/tsconfig": "*", + "@types/react": "^18.2.18", + "@types/react-dom": "^18.2.7", + "eslint": "^8.46.0", + "react": "^18.2.0", + "typescript": "^5.1.6" + }, + "peerDependencies": { + "@storybook/addon-actions": ">=6", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@storybook/addon-actions": { + "optional": true + } + } + }, "packages/tsconfig": { "name": "@codedazur/tsconfig", "version": "0.0.5", diff --git a/packages/react-tracking/CHANGELOG.md b/packages/react-tracking/CHANGELOG.md new file mode 100644 index 00000000..f4267ec4 --- /dev/null +++ b/packages/react-tracking/CHANGELOG.md @@ -0,0 +1,39 @@ +# @codedazur/react-notifications + +## 0.1.3 + +### Patch Changes + +- [`3fa49d9`](https://github.com/codedazur/toolkit/commit/3fa49d9873760a102ae359a10fe2a8d76b27f432) Thanks [@thijsdaniels](https://github.com/thijsdaniels)! - Remove redundant readonly marker from props. + +## 0.1.2 + +### Patch Changes + +- [`8c88544`](https://github.com/codedazur/toolkit/commit/8c885444b7bb679dbe74446f387f9ece679d51b4) Thanks [@thijsdaniels](https://github.com/thijsdaniels)! - The children and onDismiss props are now optional. + +## 0.1.1 + +### Patch Changes + +- [`6ed81ad`](https://github.com/codedazur/toolkit/commit/6ed81ad99d123a2cfa1618e63db5b4e10b98b0f0) Thanks [@thijsdaniels](https://github.com/thijsdaniels)! - The NotificationProps have been simplified. + +## 0.1.0 + +### Minor Changes + +- [#122](https://github.com/codedazur/toolkit/pull/122) [`e6aeec9`](https://github.com/codedazur/toolkit/commit/e6aeec9223af755d873e0d02337743cda5b2fb9c) Thanks [@sayinserdar](https://github.com/sayinserdar)! - react-notifications: clear function is added + +## 0.0.2 + +### Patch Changes + +- b27ce3a: add react essentials as dep + +## 0.0.1 + +### Patch Changes + +- 369cc3d: add changesets +- Updated dependencies [369cc3d] + - @codedazur/essentials@0.0.1 diff --git a/packages/react-tracking/LICENSE.md b/packages/react-tracking/LICENSE.md new file mode 100644 index 00000000..811b1d9f --- /dev/null +++ b/packages/react-tracking/LICENSE.md @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (c) 2022 Code d'Azur Interactive B.V. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/react-tracking/README.md b/packages/react-tracking/README.md new file mode 100644 index 00000000..8282af91 --- /dev/null +++ b/packages/react-tracking/README.md @@ -0,0 +1 @@ +# @codedazur/react-tracking diff --git a/packages/react-tracking/contexts/trackingContext.ts b/packages/react-tracking/contexts/trackingContext.ts new file mode 100644 index 00000000..e067a90a --- /dev/null +++ b/packages/react-tracking/contexts/trackingContext.ts @@ -0,0 +1,112 @@ +import { MouseEvent, SyntheticEvent, createContext } from "react"; +import { Tracker } from "../providers/TrackingProvider"; + +export interface TrackingContext { + path: string[]; + tracker: Tracker | false; + track: (event: TrackingEvent) => void; + trackElement: (type: string, element: HTMLElement) => void; + trackEvent: (type: string, event: SyntheticEvent) => void; + trackNavigate: (data: Partial) => void; + trackClick: (event: MouseEvent) => void; + trackEnter: (element: HTMLElement) => void; + trackExit: (element: HTMLElement) => void; + trackLoad: (event: SyntheticEvent) => void; +} + +export enum TrackingEventType { + navigate = "navigate", + click = "click", + enter = "enter", + exit = "exit", + load = "load", +} + +export interface EventMetadata { + timestamp: number; + path: string; +} + +export type TrackingEvent = BaseEvent | PageEvent | ElementEvent; + +export interface BaseEvent { + type: string; + data: object; +} + +export interface PageEvent extends BaseEvent { + type: string; + data: { + page: PageData; + }; +} + +export interface ElementEvent extends BaseEvent { + type: string; + data: { + page: PageData; + element: ElementData; + }; +} + +export interface PageData { + host: string; + path: string; + title: string; +} + +export type ElementData = + | BaseElementData + | AnchorElementData + | ImageElementData + | MediaElementData + | FrameElementData; + +export interface BaseElementData { + tag: string; + id: string | null; + path: string; + label: string | null; + text: string | null; +} + +export interface AnchorElementData extends BaseElementData { + anchor: { + destination: string; + }; +} + +export interface ImageElementData extends BaseElementData { + image: { + source: string; + description: string; + }; +} + +export interface MediaElementData extends BaseElementData { + media: { + source: string; + duration: number; + time: number; + progress: number; + }; +} + +export interface FrameElementData extends BaseElementData { + frame: { + source: string; + }; +} + +export const trackingContext = createContext({ + path: [], + tracker: false, + track: () => undefined, + trackElement: () => undefined, + trackEvent: () => undefined, + trackNavigate: () => undefined, + trackClick: () => undefined, + trackEnter: () => undefined, + trackExit: () => undefined, + trackLoad: () => undefined, +}); diff --git a/packages/react-tracking/hooks/useIntersectionTracker.ts b/packages/react-tracking/hooks/useIntersectionTracker.ts new file mode 100644 index 00000000..19c461a2 --- /dev/null +++ b/packages/react-tracking/hooks/useIntersectionTracker.ts @@ -0,0 +1,68 @@ +import { + MaybeRef, + resolveMaybeRef, + useIsIntersecting, + usePrevious, +} from "@codedazur/react-essentials"; +import { useEffect, useRef } from "react"; +import { useTracker } from "./useTracker"; + +export function useIntersectionTracker( + ref: MaybeRef, + { + threshold = 0.5, + frequency = "once", + trackEnter: shouldTrackEnter = true, + trackExit: shouldTrackExit = false, + }: { + threshold?: number; + frequency?: "once" | "always"; + trackEnter?: boolean; + trackExit?: boolean; + } = {}, +) { + const isIntersecting = useIsIntersecting(ref, { threshold }); + const wasIntersecting = usePrevious(isIntersecting); + const hasTrackedEnter = useRef(false); + const hasTrackedExit = useRef(false); + const { trackEnter, trackExit } = useTracker(); + + useEffect(() => { + const element = resolveMaybeRef(ref); + + if (!element) { + return; + } + + if (wasIntersecting === undefined) { + return; + } + + if ( + isIntersecting && + shouldTrackEnter && + (frequency === "always" || hasTrackedEnter.current === false) + ) { + trackEnter(element); + hasTrackedEnter.current = true; + } else if ( + !isIntersecting && + shouldTrackExit && + (frequency === "always" || hasTrackedExit.current === false) + ) { + trackExit(element); + hasTrackedExit.current = true; + } + }, [ + frequency, + hasTrackedEnter, + hasTrackedExit, + isIntersecting, + ref, + shouldTrackEnter, + shouldTrackExit, + trackEnter, + trackExit, + wasIntersecting, + ]); +} diff --git a/packages/react-tracking/hooks/useLoadTracker.ts b/packages/react-tracking/hooks/useLoadTracker.ts new file mode 100644 index 00000000..d6a16108 --- /dev/null +++ b/packages/react-tracking/hooks/useLoadTracker.ts @@ -0,0 +1,24 @@ +import { MaybeRef, resolveMaybeRef } from "@codedazur/react-essentials"; +import { SyntheticEvent, useEffect } from "react"; +import { useTracker } from "./useTracker"; + +export function useLoadTracker(ref: MaybeRef) { + const { trackLoad } = useTracker(); + + useEffect(() => { + const element = resolveMaybeRef(ref); + + if (!element) { + return; + } + + const handleLoad = (event: Event) => + trackLoad(event as unknown as SyntheticEvent); + + element.addEventListener("load", handleLoad); + + return () => { + element.removeEventListener("load", handleLoad); + }; + }, [ref, trackLoad]); +} diff --git a/packages/react-tracking/hooks/useMediaTracker.ts b/packages/react-tracking/hooks/useMediaTracker.ts new file mode 100644 index 00000000..ddeaf4da --- /dev/null +++ b/packages/react-tracking/hooks/useMediaTracker.ts @@ -0,0 +1,103 @@ +import { MaybeRef, resolveMaybeRef } from "@codedazur/react-essentials"; +import { useEffect, useRef } from "react"; +import { useTracker } from "./useTracker"; + +export function useMediaTracker( + ref: MaybeRef, + { percentiles = 0.25 }: { percentiles?: number | false } = {}, +) { + const { trackElement } = useTracker(); + const hasTrackedStart = useRef(false); + const isSeeking = useRef(false); + const progress = useRef(0); + const largestProgress = useRef(0); + + useEffect(() => { + const element = resolveMaybeRef(ref); + + if (!element) { + return; + } + + const handlePlay = () => { + isSeeking.current = false; + + if (hasTrackedStart.current === false) { + trackElement("media.start", element); + hasTrackedStart.current = true; + } else { + trackElement("media.resume", element); + } + }; + + const handleSeeking = () => { + isSeeking.current = true; + }; + + const handlePause = () => { + if (element.currentTime === element.duration) { + return; + } + + trackElement("media.pause", element); + }; + + const handleEnded = () => { + if (isSeeking.current) { + return; + } + + trackElement("media.end", element); + }; + + const handleTimeUpdate = () => { + if (!percentiles) { + return; + } + + const newProgress = element.currentTime / element.duration; + + if ( + !isSeeking.current && + newProgress > largestProgress.current && + newProgress < 1 + ) { + const newPercentile = Math.floor(newProgress / percentiles); + const currentPercentile = Math.floor(progress.current / percentiles); + + if (newPercentile > currentPercentile) { + trackElement( + `media.progress[${Math.round(newPercentile * percentiles * 100)}]`, + element, + ); + } + } + + progress.current = newProgress; + + if (newProgress > largestProgress.current) { + largestProgress.current = newProgress; + } + }; + + element.addEventListener("play", handlePlay); + element.addEventListener("seeking", handleSeeking); + element.addEventListener("pause", handlePause); + element.addEventListener("ended", handleEnded); + + if (percentiles !== false) { + element.addEventListener("timeupdate", handleTimeUpdate); + } + + return () => { + element.removeEventListener("play", handlePlay); + element.removeEventListener("pause", handlePause); + element.removeEventListener("seeking", handleSeeking); + element.removeEventListener("ended", handleEnded); + + if (percentiles !== false) { + element.removeEventListener("timeupdate", handleTimeUpdate); + } + }; + }, [percentiles, ref, trackElement]); +} diff --git a/packages/react-tracking/hooks/useTracker.ts b/packages/react-tracking/hooks/useTracker.ts new file mode 100644 index 00000000..92b59f3f --- /dev/null +++ b/packages/react-tracking/hooks/useTracker.ts @@ -0,0 +1,7 @@ +import { useContext } from "react"; +import { TrackingContext, trackingContext } from "../contexts/trackingContext"; + +export const useTracker = (): Omit => { + const { tracker, ...rest } = useContext(trackingContext); + return rest; +}; diff --git a/packages/react-tracking/index.ts b/packages/react-tracking/index.ts new file mode 100644 index 00000000..d1c9e60c --- /dev/null +++ b/packages/react-tracking/index.ts @@ -0,0 +1,8 @@ +export * from "./contexts/trackingContext"; +export * from "./hooks/useIntersectionTracker"; +export * from "./hooks/useLoadTracker"; +export * from "./hooks/useMediaTracker"; +export * from "./hooks/useTracker"; +export * from "./utilities/createConsoleTracker"; +export * from "./utilities/createGtmTracker"; +export * from "./utilities/createStorybookTracker"; diff --git a/packages/react-tracking/package.json b/packages/react-tracking/package.json new file mode 100644 index 00000000..03fa8d81 --- /dev/null +++ b/packages/react-tracking/package.json @@ -0,0 +1,46 @@ +{ + "name": "@codedazur/react-tracking", + "version": "0.0.0", + "main": ".dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "require": "./dist/index.js", + "import": "./dist/index.mjs" + } + }, + "publishConfig": { + "access": "public" + }, + "license": "MIT", + "scripts": { + "develop": "tsup index.ts --format esm,cjs --dts --watch --external react", + "build": "tsup index.ts --format esm,cjs --dts", + "lint": "TIMING=1 eslint \"**/*.ts*\"", + "types": "tsc --noEmit", + "test": "vitest run --passWithNoTests" + }, + "peerDependencies": { + "react": ">=16.8.0", + "@storybook/addon-actions": ">=6" + }, + "peerDependenciesMeta": { + "@storybook/addon-actions": { + "optional": true + } + }, + "dependencies": { + "@codedazur/essentials": "*", + "@codedazur/react-essentials": "*" + }, + "devDependencies": { + "@codedazur/eslint-config": "*", + "@codedazur/tsconfig": "*", + "@types/react-dom": "^18.2.7", + "@types/react": "^18.2.18", + "eslint": "^8.46.0", + "react": "^18.2.0", + "typescript": "^5.1.6" + } +} diff --git a/packages/react-tracking/providers/TrackingProvider.tsx b/packages/react-tracking/providers/TrackingProvider.tsx new file mode 100644 index 00000000..6449d70c --- /dev/null +++ b/packages/react-tracking/providers/TrackingProvider.tsx @@ -0,0 +1,255 @@ +import { + FunctionComponent, + MouseEvent, + ReactNode, + SyntheticEvent, + useCallback, + useContext, + useMemo, +} from "react"; +import { + AnchorElementData, + BaseElementData, + BaseEvent, + ElementData, + ElementEvent, + EventMetadata, + FrameElementData, + ImageElementData, + MediaElementData, + PageData, + PageEvent, + trackingContext, + TrackingEventType, +} from "../contexts/trackingContext"; + +export interface TrackingProviderProps { + slug: string; + tracker?: Tracker | false; + children?: ReactNode; +} + +export type Tracker = (event: BaseEvent & EventMetadata) => void; + +export const TrackingProvider: FunctionComponent = ({ + slug, + tracker, + children, +}) => { + const parent = usePrivateTracker(); + + const inheritedTracker = useMemo( + () => (tracker === undefined ? parent.tracker : tracker), + [parent.tracker, tracker], + ); + + const path = useMemo(() => [...parent.path, slug], [parent.path, slug]); + + const track = useCallback( + (event: E) => + inheritedTracker instanceof Function && + inheritedTracker({ + timestamp: new Date().getTime(), + path: path.join("."), + ...event, + }), + [inheritedTracker, path], + ); + + const trackElement = useCallback( + (type: string, element: HTMLElement) => { + track({ + type, + data: { + page: getDataForPage(), + element: getDataForElement(element), + }, + }); + }, + [track], + ); + + const trackEvent = useCallback( + (type: string, event: SyntheticEvent) => { + trackElement(type, event.currentTarget as HTMLElement); + }, + [trackElement], + ); + + const trackNavigate = useCallback( + (data: Partial) => { + track({ + type: TrackingEventType.navigate, + data: { + page: { ...getDataForPage(), ...data }, + }, + }); + }, + [track], + ); + + const trackClick = useCallback( + (event: MouseEvent) => { + trackEvent(TrackingEventType.click, event); + }, + [trackEvent], + ); + + const trackEnter = useCallback( + (element: HTMLElement) => { + trackElement(TrackingEventType.enter, element); + }, + [trackElement], + ); + + const trackExit = useCallback( + (element: HTMLElement) => { + trackElement(TrackingEventType.exit, element); + }, + [trackElement], + ); + + const trackLoad = useCallback( + (event: SyntheticEvent) => { + trackEvent(TrackingEventType.load, event); + }, + [trackEvent], + ); + + return ( + + {children} + + ); +}; + +const usePrivateTracker = () => useContext(trackingContext); + +function getDataForPage(): PageData { + return { + host: window.location.hostname, + path: window.location.pathname, + title: window.document.title, + }; +} + +function getDataForElement(element: HTMLElement): ElementData { + if (element instanceof HTMLAnchorElement) { + return anchorElementData(element); + } else if (element instanceof HTMLImageElement) { + return imageElementData(element); + } else if (element instanceof HTMLMediaElement) { + return mediaElementData(element); + } else if (element instanceof HTMLIFrameElement) { + return frameElementData(element); + } else { + return elementData(element); + } +} + +function elementData(element: HTMLElement): BaseElementData { + return { + tag: element.tagName.toLowerCase(), + id: element.id || null, + /** + * @todo Decide whether the cost of calculating the `cssPath` is worth the + * business value. Maybe the `context` is good enough. + */ + path: cssPath(element), + label: element.title || element.ariaLabel || null, + text: element.textContent || null, + }; +} + +function anchorElementData(element: HTMLAnchorElement): AnchorElementData { + return { + ...elementData(element), + anchor: { + destination: element.href, + }, + }; +} + +function imageElementData(element: HTMLImageElement): ImageElementData { + return { + ...elementData(element), + image: { + source: element.srcset || element.src, + description: element.alt, + }, + }; +} + +function mediaElementData(element: HTMLMediaElement): MediaElementData { + return { + ...elementData(element), + media: { + source: element.currentSrc, + duration: element.duration, + time: element.currentTime, + progress: element.currentTime / element.duration, + }, + }; +} + +function frameElementData(element: HTMLIFrameElement): FrameElementData { + return { + ...elementData(element), + frame: { + source: element.src, + }, + }; +} + +/** + * @todo Move this to a utility directory or consider exporting this as part of + * the `@codedazur/essentials` package. + * @todo Write unit tests for this function. + * @todo Determine the performance cost of this function. + */ +function cssPath(element: HTMLElement) { + const path = []; + + let node: Node | null = element; + + while (node instanceof Element) { + let selector = node.nodeName.toLowerCase(); + + if (node.id) { + selector += "#" + node.id; + path.unshift(selector); + break; + } else { + let sibling: Element | null = node; + let n = 1; + + while ((sibling = sibling.previousElementSibling)) { + if (sibling.nodeName.toLowerCase() == selector) { + n++; + } + } + + if (n != 1) { + selector += `:nth-of-type(${n})`; + } + } + + path.unshift(selector); + node = node.parentNode; + } + + return path.join(" > "); +} diff --git a/packages/react-tracking/tsconfig.json b/packages/react-tracking/tsconfig.json new file mode 100644 index 00000000..91ca394d --- /dev/null +++ b/packages/react-tracking/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@codedazur/tsconfig/react-library.json", + "include": ["."], + "exclude": ["dist", "build", "node_modules"] +} diff --git a/packages/react-tracking/utilities/createConsoleTracker.ts b/packages/react-tracking/utilities/createConsoleTracker.ts new file mode 100644 index 00000000..c3711438 --- /dev/null +++ b/packages/react-tracking/utilities/createConsoleTracker.ts @@ -0,0 +1,9 @@ +import { Tracker } from "../providers/TrackingProvider"; + +export function createConsoleTracker({ + method = "dir", +}: { method?: "log" | "info" | "dir" } = {}): Tracker { + return function logTracker(event) { + console[method](event); + }; +} diff --git a/packages/react-tracking/utilities/createGtmTracker.ts b/packages/react-tracking/utilities/createGtmTracker.ts new file mode 100644 index 00000000..eae57257 --- /dev/null +++ b/packages/react-tracking/utilities/createGtmTracker.ts @@ -0,0 +1,18 @@ +import { Tracker } from "../providers/TrackingProvider"; + +declare global { + interface Window { + dataLayer: Array>; + } +} + +export function createGtmTracker({ + prefix, +}: { prefix?: string } = {}): Tracker { + return function gtmTracker({ type, ...event }) { + window.dataLayer.push({ + event: [prefix, type].filter(Boolean).join("."), + ...event, + }); + }; +} diff --git a/packages/react-tracking/utilities/createStorybookTracker.ts b/packages/react-tracking/utilities/createStorybookTracker.ts new file mode 100644 index 00000000..73a63c3c --- /dev/null +++ b/packages/react-tracking/utilities/createStorybookTracker.ts @@ -0,0 +1,17 @@ +import { Tracker } from "../providers/TrackingProvider"; + +declare global { + interface Window { + dataLayer: Array>; + } +} + +export async function createStorybookTracker({ + name = "Tracker", +}: { name?: string } = {}): Promise { + const { action } = await import("@storybook/addon-actions"); + + return function storybookTracker(event) { + action(name)(event); + }; +}