Skip to content

Commit

Permalink
Merge pull request #546 from input-output-hk/filip/feat/posthog
Browse files Browse the repository at this point in the history
filip(feat): add posthog integration
  • Loading branch information
fstoqnov-iohk authored Nov 20, 2023
2 parents 54afbeb + 159384d commit 4cba9c4
Show file tree
Hide file tree
Showing 8 changed files with 292 additions and 39 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
"gatsby-plugin-manifest": "^2.4.28",
"gatsby-plugin-mdx": "^1.0.61",
"gatsby-plugin-offline": "^3.0.29",
"dayjs": "^1.11.9",
"posthog-js": "^1.76.0",
"gatsby-plugin-react-helmet": "^3.1.18",
"gatsby-plugin-remove-serviceworker": "^1.0.0",
"gatsby-plugin-sharp": "^2.6.35",
Expand Down
160 changes: 160 additions & 0 deletions src/analytics/AnalyticsContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import React, { createContext, useCallback, useEffect, useState } from 'react'
import dayjs from 'dayjs'
import { posthog } from 'posthog-js'
import { PostHogProvider } from 'posthog-js/react'
import TrackRoute from './TrackRoute'
import useHasConsent, { ConsentType } from './useHasConsent'

/**
* @file This file exports an AnalyticsContext and an AnalyticsProvider component.
* The AnalyticsContext is a context object that provides a function to capture analytics events.
* The AnalyticsProvider is a component that provides the PostHog client and wraps its children with the AnalyticsContext.
* @module AnalyticsContext
*/

/**
* A context object that provides a function to capture analytics events.
* @typedef {Object} AnalyticsContext
* @property {Function} capture - A function that captures an analytics event.
* @param {string} eventName - The name of the event to capture.
* @param {Object} [eventProps] - An optional object containing additional properties to include in the event.
*/

/**
* A component that provides the PostHog client and wraps its children with the AnalyticsContext.
* @typedef {Object} AnalyticsProvider
* @property {Object} children - The child components to wrap with the AnalyticsContext.
*/

/**
* The base event properties to include in all analytics events.
* @typedef {Function} BaseEventProps
* @returns {Object} An object containing the base event properties.
*/

/**
* A hook that returns the base event properties to include in all analytics events.
* @typedef {Function} UseBaseEventProps
* @returns {BaseEventProps} A function that returns an object containing the base event properties.
*/

/**
* A hook that returns a function to capture analytics events.
* @typedef {Function} UseAnalyticsCapture
* @returns {Function} A function that captures an analytics event.
* @param {string} eventName - The name of the event to capture.
* @param {Object} [eventProps] - An optional object containing additional properties to include in the event.
*/

/**
* A hook that returns the PostHog client.
* @typedef {Function} UsePostHogClient
* @returns {Object} The PostHog client.
*/

/**
* A hook that returns whether the user has accepted analytics consent.
* @typedef {Function} UseHasAnalyticsConsent
* @returns {boolean} Whether the user has accepted analytics consent.
*/

/**
* A hook that returns the PostHog client and a function to capture analytics events.
* @typedef {Function} UseAnalytics
* @returns {Array} An array containing the PostHog client and a function to capture analytics events.
* @param {string} eventName - The name of the event to capture.
* @param {Object} [eventProps] - An optional object containing additional properties to include in the event.
*/

/**
* The props for the AnalyticsProvider component.
* @typedef {Object} AnalyticsProviderProps
* @property {Object} children - The child components to wrap with the AnalyticsContext.
*/

/**
* The props for the AnalyticsContextProvider component.
* @typedef {Object} AnalyticsContextProviderProps
* @property {Function} capture - A function that captures an analytics event.
* @param {string} eventName - The name of the event to capture.
* @param {Object} [eventProps] - An optional object containing additional properties to include in the event.
* @property {Object} children - The child components to wrap with the AnalyticsContext.
*/
export const AnalyticsContext = createContext(() => {})

export function AnalyticsProvider({ children }) {
const [client, setClient] = useState()

const analyticsAccepted = useHasConsent(ConsentType.ANALYTICS)

const baseEventProps = useCallback(
() => ({
sent_at_local: dayjs().format(),
posthog_project_id: process.env.GATSBY_POSTHOG_PROJECT_ID,
}),
[],
)

const capture = useCallback(
(eventName, eventProperties = {}) => {
if (client) {
client.capture(eventName, {
...baseEventProps(),
...eventProperties,
})
}
},
[client, baseEventProps],
)

useEffect(() => {
const posthogApiKey = process.env.GATSBY_POSTHOG_API_KEY

const posthogApiHost = process.env.GATSBY_POSTHOG_API_HOST

const turnOn =
analyticsAccepted === true &&
typeof posthogApiKey === 'string' &&
posthogApiKey &&
typeof posthogApiHost === 'string' &&
posthogApiHost

setClient(oldClient => {
if (turnOn) {
const client =
oldClient ??
posthog.init(posthogApiKey ?? '', {
api_host: posthogApiHost,
capture_pageleave: false,
capture_pageview: false,
})

// clear localStorage state that might have been set by previous clients
client.clear_opt_in_out_capturing()

// we got consent, start capturing
client.opt_in_capturing({
capture_properties: baseEventProps(),
})
// calling a private function as a fix for bug https://github.com/PostHog/posthog-js/issues/336
client._start_queue_if_opted_in()

return client
} else {
if (oldClient) {
oldClient.opt_out_capturing()
}
return undefined
}
})
}, [analyticsAccepted, baseEventProps])

return (
<PostHogProvider client={client}>
<AnalyticsContext.Provider value={capture}>
<TrackRoute />
{children}
</AnalyticsContext.Provider>
</PostHogProvider>
)
}
15 changes: 15 additions & 0 deletions src/analytics/TrackRoute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useLocation } from '@reach/router'
import { useContext, useEffect } from 'react'
import { AnalyticsContext } from './AnalyticsContext'

export default function TrackRoute() {
const location = useLocation()

const capture = useContext(AnalyticsContext)

useEffect(() => {
capture('$pageview')
}, [capture, location.pathname, location.search])

return null
}
17 changes: 17 additions & 0 deletions src/analytics/osano.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const OsanoConsentType = {
ESSENTIAL: 'ESSENTIAL',
STORAGE: 'STORAGE',
MARKETING: 'MARKETING',
PERSONALIZATION: 'PERSONALIZATION',
ANALYTICS: 'ANALYTICS',
OPT_OUT: 'OPT_OUT',
}

export const OsanoConsentDecision = {
ACCEPT: 'ACCEPT',
DENY: 'DENY',
}

export const OsanoEvent = {
CONSENT_SAVED: 'osano-cm-consent-saved',
}
45 changes: 45 additions & 0 deletions src/analytics/useHasConsent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useEffect, useState } from 'react'
import { OsanoConsentDecision, OsanoConsentType, OsanoEvent } from './osano'

export { OsanoConsentType as ConsentType }

export default function useHasConsent(type) {
let initialConsent

try {
initialConsent =
typeof window && typeof window.Osano !== 'undefined'
? window.Osano.cm.getConsent()[type] === OsanoConsentDecision.ACCEPT
: undefined
} catch (err) {
console.log(err)
}

const [state, setState] = useState(initialConsent)

useEffect(() => {
const cm =
typeof window && typeof window.Osano !== 'undefined'
? window.Osano.cm
: undefined

if (!cm) {
return
}

setState(cm.getConsent()[type] === OsanoConsentDecision.ACCEPT)

const handler = changed => {
if (type in changed) {
setState(changed[type] === OsanoConsentDecision.ACCEPT)
}
}

cm.addEventListener(OsanoEvent.CONSENT_SAVED, handler)
return () => {
cm.removeEventListener(OsanoEvent.CONSENT_SAVED, handler)
}
}, [type])

return state
}
1 change: 0 additions & 1 deletion src/components/Footer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,6 @@ const LinksColumn = styled.div`
const Footer = ({ theme }) => {
const content = data.content
let logoURL = 'https://ucarecdn.com/75b74f03-ff04-47ba-821c-5e477d3d46d4/'

return (
<FooterSection theme={theme}>
<TopRow>
Expand Down
57 changes: 30 additions & 27 deletions src/components/layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import mdxComponents from './mdxComponents'
import Sidebar from './sidebar'
import RightSidebar from './rightSidebar'
import config from '../../config.js'
import { AnalyticsProvider } from '../analytics/AnalyticsContext.js'

const SiteWrap = styled.div`
background-color: ${({ theme }) => theme.colors.background};
Expand All @@ -21,7 +22,7 @@ const Wrapper = styled('div')`
.sideBarUL li a,
.sectionHeading {
cursor:pointer;
cursor: pointer;
color: ${({ theme }) => theme.colors.text};
}
Expand Down Expand Up @@ -78,34 +79,36 @@ const RightSideBarWidth = styled('div')`
const Layout = ({ children, location, useFwTemplate }, theme) => (
<ThemeProvider location={location}>
<MDXProvider components={mdxComponents}>
<SiteWrap>
<Wrapper>
{useFwTemplate ? (
<Content fwTemplate={useFwTemplate}>
<MaxWidth>{children}</MaxWidth>
</Content>
) : (
<>
<LeftSideBarWidth className={'hiddenMobile'}>
<Sidebar location={location} />
</LeftSideBarWidth>
{config.sidebar.title ? (
<div
className={'sidebarTitle sideBarShow'}
dangerouslySetInnerHTML={{ __html: config.sidebar.title }}
/>
) : null}
<Content>
<AnalyticsProvider>
<SiteWrap>
<Wrapper>
{useFwTemplate ? (
<Content fwTemplate={useFwTemplate}>
<MaxWidth>{children}</MaxWidth>
</Content>
<RightSideBarWidth className={'hiddenMobile'}>
{location && <RightSidebar location={location} />}
</RightSideBarWidth>
</>
)}
</Wrapper>
<Footer />
</SiteWrap>
) : (
<>
<LeftSideBarWidth className={'hiddenMobile'}>
<Sidebar location={location} />
</LeftSideBarWidth>
{config.sidebar.title ? (
<div
className={'sidebarTitle sideBarShow'}
dangerouslySetInnerHTML={{ __html: config.sidebar.title }}
/>
) : null}
<Content>
<MaxWidth>{children}</MaxWidth>
</Content>
<RightSideBarWidth className={'hiddenMobile'}>
{location && <RightSidebar location={location} />}
</RightSideBarWidth>
</>
)}
</Wrapper>
<Footer />
</SiteWrap>
</AnalyticsProvider>
</MDXProvider>
</ThemeProvider>
)
Expand Down
Loading

0 comments on commit 4cba9c4

Please sign in to comment.