diff --git a/doc/gtag_setup.md b/doc/gtag_setup.md index 38a8368..6ecd0aa 100644 --- a/doc/gtag_setup.md +++ b/doc/gtag_setup.md @@ -22,3 +22,18 @@ const App = () => { ); }; ``` + + +## Usage + +```js +import { + EventTypes, + pushGaEvent, +} from '@freshheads/analytics-essentials'; + +pushGaEvent({ + type: EventTypes.CLICK, + name: 'hero_button_click' +}); +``` diff --git a/doc/mixpanel_setup.md b/doc/mixpanel_setup.md new file mode 100644 index 0000000..56d5328 --- /dev/null +++ b/doc/mixpanel_setup.md @@ -0,0 +1,149 @@ +# Mixpanel setup + +## Introduction +This implementation of mixpanel relies on a backend service to send the data to mixpanel. The goal of the frontend lib is to provide a typechecked event to help developers push events the same way. +For the backend Freshheads created a php bundle, [FHMixpanelBundle](https://github.com/freshheads/FHMixpanelBundle) and a node library will also be created in the future. + +## Usage + +Add the MixpanelProvider to your app: + +```tsx +import { MixpanelProvider } from '@freshheads/analytics-essentials'; + +const App = () => { + return ( + + + + ); +}; +``` + +`sendTrackEvent` is a function that sends the event to the backend. It should have the following signature: + +```tsx + +import { MixpanelEvent } from '@freshheads/analytics-essentials'; +import { executePostRequest } from '@/api/client'; + +export const sendTrackEvent = async (data: MixpanelEvent) => { + return executePostRequest('_mixpanel/track', data); +}; +``` + +Then you can use the `useMixpanelContext` hook to send events: + +```tsx +import { useMixpanelContext } from '@freshheads/analytics-essentials'; + +const { trackEvent } = useMixpanelContext(); + +trackEvent({ + name: 'Add to cart', + data: { + product_name: product.title, + }, +}); +``` + +### Context and overrides +By default, each event will have a context property that contains more information about the page it was sent from. +This is resolved from window.location because we have no access to the router state. + +``` +{ + title: 'Product Page', // What page is the event triggered on + pathname: '/product/123', // Make sure there aren't any personal info in the path + href: 'https://www.example.com/product/123', // Make sure there aren't any personal info in the href +} +``` + +You can override this context by providing a `context` property to the event. + +```tsx +trackEvent({ + name: 'Add to cart', + context: { + section: 'footer', + }, +}); +``` + +Or by providing a default context to the MixpanelProvider: + +```tsx +const router = useRouter(); + +const defaultMixpanelEventContext = { + pathname: router.pathname, + route: router.route, + audience: 'Freelancer', +}; + +const App = () => { + return ( + + + + ); +}; +``` + +### Page view events + +For page view events, you can use the `trackPageView` function. +First create a component that will be used to track page views. +The reason we don't make this part of the essential package is because we don't want to make any assumptions about the router you are using. +Or you might want to send additional data with the page view event. + +```tsx +export default function TrackPageView() { + const { trackPageView } = useMixpanelContext(); + const router = useRouter(); + const pathname = usePathname(); + + useEffect(() => { + trackPageView({ + data: { + title: document.title, + pathname: pathname, + route: router.route, + }, + }); + }, [pathname]); + + return null; +} +``` + +Then add this component to your app: + +```tsx + +const App = () => { + return ( + + + {children} + + ); +}; +``` + +### UTM tracking + +UTM tags are automatically added to the context of the event if they are present in the URL. +They will be remembered for the duration of the session. Even if the user navigates to a different page, the UTM tags will be added to new events. + +## Mixpanel users + +TODO how to handle reset mixpanel user + +## Event naming conventions + +TODO - how to name events + +## Event types + +TODO - how to override the event types diff --git a/doc/tagmanager_setup.md b/doc/tagmanager_setup.md index d7e0002..8cf094e 100644 --- a/doc/tagmanager_setup.md +++ b/doc/tagmanager_setup.md @@ -17,3 +17,31 @@ const App = () => { ); }; ``` + +## Usage + +```js +import { + EventTypes, + pushDataLayerEvent, +} from '@freshheads/analytics-essentials'; + +pushDataLayerEvent({ + type: EventTypes.CLICK, + name: 'hero_button_click', +}); +``` + +## Optional params + +```js +pushDataLayerEvent({ + type: EventTypes.CLICK, + name: 'hero_button_click', + context: { + // context is typed based on type + // you can always extend context with your custom properties + } +}), + +``` diff --git a/lib/mixpanel/context.tsx b/lib/mixpanel/context.tsx index 26ec2b2..5e8e63f 100644 --- a/lib/mixpanel/context.tsx +++ b/lib/mixpanel/context.tsx @@ -1,23 +1,26 @@ 'use client'; import React, { createContext, useContext, useEffect } from 'react'; -import { MixpanelEvent as DTO } from './types'; +import { MixpanelEvent, MixpanelPageViewEvent } from './types'; import { extractUtmParams, isStandalonePWA, writeUtmParamsToSessionStorage, } from './utils.ts'; -interface MixpanelContextProps { +interface MixpanelContextProps { trackEvent: (event: DTO) => void; + trackPageView: (event: MixpanelPageViewEvent) => void; } -interface MixpanelProviderProps { +interface MixpanelProviderProps { children: React.ReactNode; eventApiClient: (args: DTO) => Promise; + defaultEventContext?: DTO['context']; } -const MixpanelContext = createContext | null>(null); +const MixpanelContext = + createContext | null>(null); export function useMixpanelContext() { const context = useContext(MixpanelContext); @@ -32,8 +35,9 @@ export function useMixpanelContext() { export function MixpanelProvider({ children, eventApiClient, -}: MixpanelProviderProps) { - const trackEvent = (event: DTO) => { + defaultEventContext, +}: MixpanelProviderProps) { + const trackEvent = (event: MixpanelEvent) => { // only send events on the client if (typeof window === 'undefined') { return; @@ -45,8 +49,27 @@ export function MixpanelProvider({ ...event, context: { title: document.title, - href: window.location.href, - path: window.location.pathname, + pathname: window.location.pathname, + pwa: isStandalonePWA(), + ...defaultEventContext, + ...utmParams, + ...event.context, + }, + }).catch((e) => console.error(e)); + }; + + const trackPageView = (event: MixpanelPageViewEvent) => { + // only send events on the client + if (typeof window === 'undefined') { + return; + } + + const utmParams = extractUtmParams(window.location.search); + + eventApiClient({ + ...event, + name: 'Page view', + context: { pwa: isStandalonePWA(), ...utmParams, ...event.context, @@ -62,6 +85,7 @@ export function MixpanelProvider({ {children} diff --git a/lib/mixpanel/types.ts b/lib/mixpanel/types.ts index cc3d59c..69640c1 100644 --- a/lib/mixpanel/types.ts +++ b/lib/mixpanel/types.ts @@ -4,14 +4,15 @@ * the other properties are suggestions for a "normal" event. * @example * const event: MixpanelEvent = { - * name: 'Contact', // e.g. "Update profile", "Add to cart", "Purchase", "Page view" + * name: 'Contact', // e.g. "Update profile", "Add to cart", "Purchase" * context: { // Give some context to the event. Where is it triggered and by who * title: 'Product Page', // What page is the event triggered on - * path: '/product/123', // Make sure there aren't any personal info in the path + * pathname: '/product/123', // Make sure there aren't any personal info in the path * href: 'https://www.example.com/product/123', // Make sure there aren't any personal info in the href * route: '/product/:id', * audience: 'Freelancer', // Who is triggering this event e.g. a role or "new user" * section: 'footer', // What section is the event triggered in + * pwa: true, // Is the event triggered in a PWA * utm_source: 'Facebook', // track the source where traffic is coming from, including a website or advertiser * utm_medium: 'advertising', // track the advertising medium, including email and banner ads * utm_campaign: 'Black friday', // track the campaign name associated with the traffic @@ -27,7 +28,7 @@ export type MixpanelEvent = { name: string; context?: { title?: string; - path?: string; + pathname?: string; href?: string; route?: string; audience?: string; @@ -38,8 +39,53 @@ export type MixpanelEvent = { utm_campaign?: string; utm_content?: string; utm_term?: string; + [key: string]: unknown; + }; + data?: { + [key: string]: unknown; + }; +}; + +/** + * @description + * When sending a page view event to Mixpanel we will pass this event to our own backend that is used as a proxy for Mixpanel. + * It differs from the `MixpanelEvent` in that it has a fixed `name` and information about the page should be provided + * + * @example + * const event: MixpanelPageViewEvent = { + * data: { + * title: 'Product Page', // What page is the event triggered on + * pathname: '/product/123', // Make sure there aren't any personal info in the path + * href: 'https://www.example.com/product/123', // Make sure there aren't any personal info in the href + * route: '/product/:id', + * audience: 'Freelancer', // The audience that is viewing the page + * }, + * context: { // This is optional and will be mostly provided by the MixpanelProvider + * pwa: true, // Is the event triggered in a PWA + * utm_source: 'Facebook', // track the source where traffic is coming from, including a website or advertiser + * utm_medium: 'advertising', // track the advertising medium, including email and banner ads + * utm_campaign: 'Black friday', // track the campaign name associated with the traffic + * utm_content: 'cta button', //track the specific link within in an ad that a user clicked + * utm_term: 'tv sale', // track keywords associated with campaigns + * } + *} + */ +export type MixpanelPageViewEvent = { + context?: { + pwa?: boolean; + utm_source?: string; + utm_medium?: string; + utm_campaign?: string; + utm_content?: string; + utm_term?: string; + [key: string]: unknown; }; data?: { + title?: string; + pathname: string; + href?: string; + route?: string; + audience?: string; [key: string]: unknown; }; }; diff --git a/package-lock.json b/package-lock.json index 6aefb58..7d3ec5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "@freshheads/analytics-essentials", "version": "0.0.5-2", "license": "MIT", + "dependencies": { + "@freshheads/analytics-essentials": "^0.0.4" + }, "devDependencies": { "@testing-library/react": "^14.2.1", "@types/gtag.js": "^0.0.12", @@ -858,6 +861,15 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@freshheads/analytics-essentials": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@freshheads/analytics-essentials/-/analytics-essentials-0.0.4.tgz", + "integrity": "sha512-oSH4pZcMC3X0nWgPUqcgp5xhWz3g++U8flcxBErYhfq1W18makx7r0g5TwaNSxFC57h1A4n4QDvto2sASQiulA==", + "peerDependencies": { + "next": ">=13", + "react": ">=18" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -1096,8 +1108,7 @@ "node_modules/@next/env": { "version": "13.5.6", "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.6.tgz", - "integrity": "sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw==", - "dev": true + "integrity": "sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw==" }, "node_modules/@next/swc-darwin-arm64": { "version": "13.5.6", @@ -1106,7 +1117,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -1448,7 +1458,6 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", - "dev": true, "dependencies": { "tslib": "^2.4.0" } @@ -2347,7 +2356,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dev": true, "dependencies": { "streamsearch": "^1.1.0" }, @@ -2396,7 +2404,6 @@ "version": "1.0.30001596", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001596.tgz", "integrity": "sha512-zpkZ+kEr6We7w63ORkoJ2pOfBwBkY/bJrG/UZ90qNb45Isblu8wzDgevEOrRL1r9dWayHjYiiyCMEXPn4DweGQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -2459,8 +2466,7 @@ "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "dev": true + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" }, "node_modules/color-convert": { "version": "1.9.3", @@ -3794,8 +3800,7 @@ "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" }, "node_modules/glob/node_modules/brace-expansion": { "version": "2.0.1", @@ -3865,8 +3870,7 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/graphemer": { "version": "1.4.0", @@ -4437,8 +4441,7 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -4636,7 +4639,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -4793,7 +4795,6 @@ "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, "funding": [ { "type": "github", @@ -4823,7 +4824,6 @@ "version": "13.5.6", "resolved": "https://registry.npmjs.org/next/-/next-13.5.6.tgz", "integrity": "sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw==", - "dev": true, "dependencies": { "@next/env": "13.5.6", "@swc/helpers": "0.5.2", @@ -8436,8 +8436,7 @@ "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -8475,7 +8474,6 @@ "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -8603,7 +8601,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dev": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -8615,7 +8612,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "dev": true, "peer": true, "dependencies": { "loose-envify": "^1.1.0", @@ -8808,7 +8804,6 @@ "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "dev": true, "peer": true, "dependencies": { "loose-envify": "^1.1.0" @@ -8958,7 +8953,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -8997,7 +8991,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "dev": true, "engines": { "node": ">=10.0.0" } @@ -9147,7 +9140,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", - "dev": true, "dependencies": { "client-only": "0.0.1" }, @@ -9286,8 +9278,7 @@ "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -9993,7 +9984,6 @@ "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", - "dev": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" diff --git a/package.json b/package.json index afd741f..4d833cc 100644 --- a/package.json +++ b/package.json @@ -62,5 +62,8 @@ "next": { "optional": true } + }, + "dependencies": { + "@freshheads/analytics-essentials": "^0.0.4" } } diff --git a/readme.md b/readme.md index 8207ca5..82e5179 100644 --- a/readme.md +++ b/readme.md @@ -8,52 +8,8 @@ npm i @freshheads/analytics-essentials ``` -### Setup Google Tag or Google Tagmanager +### Usage by analytics provider +- [Mixpanel Setup](doc/mixpanel_setup.md) - [Gtag Setup](doc/gtag_setup.md) - [Tagmanager Setup](doc/tagmanager_setup.md) - - -## Usage - -### With gtag - -```js -import { - EventTypes, - pushGaEvent, -} from '@freshheads/analytics-essentials'; - -pushGaEvent({ - type: EventTypes.CLICK, - name: 'hero_button_click' -}); -``` - -### With Tag Manager - -```js -import { - EventTypes, - pushDataLayerEvent, -} from '@freshheads/analytics-essentials'; - -pushDataLayerEvent({ - type: EventTypes.CLICK, - name: 'hero_button_click', -}); -``` - -## Optional params - -```js -pushDataLayerEvent({ - type: EventTypes.CLICK, - name: 'hero_button_click', - context: { - // context is typed based on type - // you can always extend context with your custom properties - } -}), - -``` diff --git a/test/mixpanel.test.tsx b/test/mixpanel.test.tsx index cf098e1..024e9ac 100644 --- a/test/mixpanel.test.tsx +++ b/test/mixpanel.test.tsx @@ -5,8 +5,14 @@ import { writeUtmParamsToSessionStorage, } from '../lib/mixpanel/utils'; import { MixpanelProvider, useMixpanelContext } from '../lib/mixpanel/context'; -import { fireEvent, render, renderHook } from '@testing-library/react'; -import React from 'react'; +import { + fireEvent, + render, + renderHook, + RenderOptions, +} from '@testing-library/react'; +import React, { useEffect } from 'react'; +import { MixpanelEvent } from '../lib/mixpanel'; describe('UTM tags', () => { const urlContainingUTMParams = new URL( @@ -43,12 +49,33 @@ describe('UTM tags', () => { describe('MixpanelContext', () => { const eventApiClient = vi.fn(() => Promise.resolve()); - const ContextWrapper = ({ children }: { children: React.ReactNode }) => ( - + const ContextWrapper = ({ + children, + defaultEventContext, + }: { + children: React.ReactNode; + defaultEventContext: MixpanelEvent; + }) => ( + {children} ); + const renderWithMixpanelProvider = ( + ui: React.ReactElement, + options?: Omit + ) => { + return render(ui, { + wrapper: (props: any) => ( + + ), + ...options?.testingLibraryOptions, + }); + }; + function TrackEventTestingComponent() { const { trackEvent } = useMixpanelContext(); return ( @@ -66,6 +93,22 @@ describe('MixpanelContext', () => { ); } + function TrackPageView() { + const { trackPageView } = useMixpanelContext(); + + useEffect(() => { + trackPageView({ + data: { + title: 'Example', + pathname: '/product/1', + route: '/product/:id', + }, + }); + }, []); + + return null; + } + test('provides expected context with trackEvent function', () => { const { result } = renderHook(() => useMixpanelContext(), { wrapper: ContextWrapper, @@ -73,12 +116,44 @@ describe('MixpanelContext', () => { expect(result.current).toHaveProperty('trackEvent'); expect(typeof result.current.trackEvent).toBe('function'); + + expect(result.current).toHaveProperty('trackPageView'); + expect(typeof result.current.trackPageView).toBe('function'); }); test('trackEvent sends correct data to api client', () => { - const { getByText } = render(, { - wrapper: ContextWrapper, + const { getByText } = renderWithMixpanelProvider( + + ); + + fireEvent.click(getByText('button')); + + expect(eventApiClient).toHaveBeenCalledWith({ + name: 'event name', + context: { + title: 'Page title', + pathname: '/', + pwa: false, + }, + data: { + productId: '123', + }, }); + }); + + test('provider can extend the default context for event tracking', () => { + const defaultEventContext = { + href: 'https://example.com', + pathname: '/example', + audience: 'Consumer', + }; + + const { getByText } = renderWithMixpanelProvider( + , + { + contextWrapperProps: { defaultEventContext }, + } + ); fireEvent.click(getByText('button')); @@ -86,13 +161,30 @@ describe('MixpanelContext', () => { name: 'event name', context: { title: 'Page title', - href: window.location.href, - path: '/', + href: 'https://example.com', + pathname: '/example', pwa: false, + audience: 'Consumer', }, data: { productId: '123', }, }); }); + + test('trackPageView sends correct data to api client', () => { + renderWithMixpanelProvider(); + + expect(eventApiClient).toHaveBeenCalledWith({ + name: 'Page view', + context: { + pwa: false, + }, + data: { + title: 'Example', + pathname: '/product/1', + route: '/product/:id', + }, + }); + }); });