From ce3c250efd74b726c401529cc948216af9ed4a96 Mon Sep 17 00:00:00 2001 From: Rafa Audibert Date: Wed, 22 Jan 2025 15:22:49 -0300 Subject: [PATCH] docs: Improve documentation/logs around PostHogProvider 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 --- .../__tests__/PostHogFeature.test.jsx | 2 + react/src/context/PostHogProvider.tsx | 68 ++++++++++++------- .../context/__tests__/PostHogContext.test.jsx | 20 ++++-- .../__tests__/PostHogProvider.test.jsx | 2 +- react/src/utils/type-utils.ts | 2 + 5 files changed, 63 insertions(+), 31 deletions(-) diff --git a/react/src/components/__tests__/PostHogFeature.test.jsx b/react/src/components/__tests__/PostHogFeature.test.jsx index 8bc27afbc..ed2feb086 100644 --- a/react/src/components/__tests__/PostHogFeature.test.jsx +++ b/react/src/components/__tests__/PostHogFeature.test.jsx @@ -59,6 +59,8 @@ describe('PostHogFeature component', () => { unobserve: () => null, disconnect: () => null, }) + + // eslint-disable-next-line compat/compat window.IntersectionObserver = mockIntersectionObserver }) diff --git a/react/src/context/PostHogProvider.tsx b/react/src/context/PostHogProvider.tsx index 9fb95b78c..9b1cdfb88 100644 --- a/react/src/context/PostHogProvider.tsx +++ b/react/src/context/PostHogProvider.tsx @@ -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 | 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 & { 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; 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) { + 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]) diff --git a/react/src/context/__tests__/PostHogContext.test.jsx b/react/src/context/__tests__/PostHogContext.test.jsx index 75a8ca0ad..8ef96981f 100644 --- a/react/src/context/__tests__/PostHogContext.test.jsx +++ b/react/src/context/__tests__/PostHogContext.test.jsx @@ -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', () => () => @@ -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.' + ) }) }) diff --git a/react/src/context/__tests__/PostHogProvider.test.jsx b/react/src/context/__tests__/PostHogProvider.test.jsx index 190d6efcb..9dd1bf921 100644 --- a/react/src/context/__tests__/PostHogProvider.test.jsx +++ b/react/src/context/__tests__/PostHogProvider.test.jsx @@ -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( diff --git a/react/src/utils/type-utils.ts b/react/src/utils/type-utils.ts index a61194526..5a9a0bd70 100644 --- a/react/src/utils/type-utils.ts +++ b/react/src/utils/type-utils.ts @@ -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