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..4b29ec377 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?: never; options?: never } + | { apiKey: string; options?: Partial; client?: never } +/** + * 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