Skip to content

Commit

Permalink
docs: Improve documentation/logs around PostHogProvider
Browse files Browse the repository at this point in the history
We're now logging more reasonable logs around our PostHogProvider to help customers identify problems with their setup. We're also typing that component much better to prevent people from getting it wrong at the type level.

Inspired by #769, but not a solution
  • Loading branch information
rafaeelaudibert committed Jan 22, 2025
1 parent 43e111d commit ce3c250
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 31 deletions.
2 changes: 2 additions & 0 deletions react/src/components/__tests__/PostHogFeature.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ describe('PostHogFeature component', () => {
unobserve: () => null,
disconnect: () => null,
})

// eslint-disable-next-line compat/compat
window.IntersectionObserver = mockIntersectionObserver
})

Expand Down
68 changes: 45 additions & 23 deletions react/src/context/PostHogProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,63 @@ import posthogJs, { PostHogConfig } from 'posthog-js'
import React, { useMemo } from 'react'
import { PostHog, PostHogContext } from './PostHogContext'

export function PostHogProvider({
children,
client,
apiKey,
options,
}: {
children?: React.ReactNode
client?: PostHog | undefined
apiKey?: string | undefined
options?: Partial<PostHogConfig> | undefined
}) {
const posthog = useMemo(() => {
if (client && apiKey) {
console.warn(
'[PostHog.js] You have provided both a client and an apiKey to PostHogProvider. The apiKey will be ignored in favour of the client.'
)
}
type WithOptionalChildren<T> = T & { children?: React.ReactNode | undefined }

if (client && options) {
console.warn(
'[PostHog.js] You have provided both a client and options to PostHogProvider. The options will be ignored in favour of the client.'
)
}
/**
* Props for the PostHogProvider component.
* This is a discriminated union type that ensures mutually exclusive props:
*
* - If `client` is provided, `apiKey` and `options` must not be provided
* - If `apiKey` is provided, `client` must not be provided, and `options` is optional
*/
type PostHogProviderProps =
| { client: PostHog; apiKey?: undefined; options?: undefined }
| { apiKey: string; options?: Partial<PostHogConfig>; client?: undefined }

/**
* PostHogProvider is a React context provider for PostHog analytics.
* It can be initialized in two mutually exclusive ways:
*
* 1. By providing an existing PostHog `client` instance
* 2. By providing an `apiKey` (and optionally `options`) to create a new client
*
* These initialization methods are mutually exclusive - you must use one or the other,
* but not both simultaneously.
*/
export function PostHogProvider({ children, client, apiKey, options }: WithOptionalChildren<PostHogProviderProps>) {
const posthog = useMemo(() => {
if (client) {
if (apiKey) {
console.warn(
'[PostHog.js] You have provided both `client` and `apiKey` to `PostHogProvider`. `apiKey` will be ignored in favour of `client`.'
)
}

if (options) {
console.warn(
'[PostHog.js] You have provided both `client` and `options` to `PostHogProvider`. `options` will be ignored in favour of `client`.'
)
}

if (client.__loaded) {
console.warn('[PostHog.js] `client` was already loaded elsewhere. This may cause issues.')
}

return client
}

if (apiKey) {
if (posthogJs.__loaded) {
console.warn('[PostHog.js] was already loaded elsewhere. This may cause issues.')
console.warn('[PostHog.js] `posthog` was already loaded elsewhere. This may cause issues.')
}

posthogJs.init(apiKey, options)
return posthogJs
}

console.warn(
'[PostHog.js] No `apiKey` or `client` were provided to `PostHogProvider`. Using default global `window.posthog` instance. You must initialize it manually. This is not recommended behavior.'
)
return posthogJs
}, [client, apiKey])

Expand Down
20 changes: 13 additions & 7 deletions react/src/context/__tests__/PostHogContext.test.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as React from 'react'
import { render } from '@testing-library/react'
import { PostHogContext, PostHogProvider } from '../'
import { PostHogProvider } from '..'

describe('usePostHogContext hook', () => {
describe('PostHogContext component', () => {
given(
'render',
() => () =>
Expand All @@ -18,11 +18,17 @@ describe('usePostHogContext hook', () => {
given.render()
})

it("should not error if a client instance can't be found in the context", () => {
// used to make sure it doesn't throw an error when no client is found e.g. nextjs
given('posthog', () => undefined)
console.error = jest.fn()
it("should not throw error if a client instance can't be found in the context", () => {
given('posthog', () => undefined) // it might not exist in SSR for example

expect(() => given.render())
// eslint-disable-next-line no-console
console.warn = jest.fn()

expect(() => given.render()).not.toThrow()

// eslint-disable-next-line no-console
expect(console.warn).toHaveBeenCalledWith(
'[PostHog.js] No `apiKey` or `client` were provided to `PostHogProvider`. Using default global `window.posthog` instance. You must initialize it manually. This is not recommended behavior.'
)
})
})
2 changes: 1 addition & 1 deletion react/src/context/__tests__/PostHogProvider.test.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react'
import { render } from '@testing-library/react'
import { PostHogProvider, PostHogContext } from '..'
import { PostHogProvider } from '..'

describe('PostHogProvider component', () => {
given(
Expand Down
2 changes: 2 additions & 0 deletions react/src/utils/type-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ export const isFunction = function (f: any): f is (...args: any[]) => any {
// eslint-disable-next-line posthog-js/no-direct-function-check
return typeof f === 'function'
}

export const isUndefined = function (x: unknown): x is undefined {
return x === void 0
}

export const isNull = function (x: unknown): x is null {
// eslint-disable-next-line posthog-js/no-direct-null-check
return x === null
Expand Down

0 comments on commit ce3c250

Please sign in to comment.