@posthog/react 1.2.2 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ export * from './PostHogFeature'
2
+ export {
3
+ PostHogErrorBoundary,
4
+ PostHogErrorBoundaryProps,
5
+ PostHogErrorBoundaryFallbackProps,
6
+ } from './PostHogErrorBoundary'
@@ -0,0 +1,6 @@
1
+ import posthogJs from 'posthog-js'
2
+ import { createContext } from 'react'
3
+
4
+ export type PostHog = typeof posthogJs
5
+
6
+ export const PostHogContext = createContext<{ client: PostHog }>({ client: posthogJs })
@@ -0,0 +1,128 @@
1
+ /* eslint-disable no-console */
2
+ import posthogJs, { PostHogConfig } from 'posthog-js'
3
+
4
+ import React, { useEffect, useMemo, useRef } from 'react'
5
+ import { PostHog, PostHogContext } from './PostHogContext'
6
+ import { isDeepEqual } from '../utils/object-utils'
7
+
8
+ interface PreviousInitialization {
9
+ apiKey: string
10
+ options: Partial<PostHogConfig>
11
+ }
12
+
13
+ type WithOptionalChildren<T> = T & { children?: React.ReactNode | undefined }
14
+
15
+ /**
16
+ * Props for the PostHogProvider component.
17
+ * This is a discriminated union type that ensures mutually exclusive props:
18
+ *
19
+ * - If `client` is provided, `apiKey` and `options` must not be provided
20
+ * - If `apiKey` is provided, `client` must not be provided, and `options` is optional
21
+ */
22
+ type PostHogProviderProps =
23
+ | { client: PostHog; apiKey?: never; options?: never }
24
+ | { apiKey: string; options?: Partial<PostHogConfig>; client?: never }
25
+
26
+ /**
27
+ * PostHogProvider is a React context provider for PostHog analytics.
28
+ * It can be initialized in two mutually exclusive ways:
29
+ *
30
+ * 1. By providing an existing PostHog `client` instance
31
+ * 2. By providing an `apiKey` (and optionally `options`) to create a new client
32
+ *
33
+ * These initialization methods are mutually exclusive - you must use one or the other,
34
+ * but not both simultaneously.
35
+ *
36
+ * We strongly suggest you memoize the `options` object to ensure that you don't
37
+ * accidentally trigger unnecessary re-renders. We'll properly detect if the options
38
+ * have changed and only call `posthogJs.set_config` if they have, but it's better to
39
+ * avoid unnecessary re-renders in the first place.
40
+ */
41
+ export function PostHogProvider({ children, client, apiKey, options }: WithOptionalChildren<PostHogProviderProps>) {
42
+ // Used to detect if the client was already initialized
43
+ // This is used to prevent double initialization when running under React.StrictMode
44
+ // We're not storing a simple boolean here because we want to be able to detect if the
45
+ // apiKey or options have changed.
46
+ const previousInitializationRef = useRef<PreviousInitialization | null>(null)
47
+
48
+ const posthog = useMemo(() => {
49
+ if (client) {
50
+ if (apiKey) {
51
+ console.warn(
52
+ '[PostHog.js] You have provided both `client` and `apiKey` to `PostHogProvider`. `apiKey` will be ignored in favour of `client`.'
53
+ )
54
+ }
55
+ if (options) {
56
+ console.warn(
57
+ '[PostHog.js] You have provided both `client` and `options` to `PostHogProvider`. `options` will be ignored in favour of `client`.'
58
+ )
59
+ }
60
+ return client
61
+ }
62
+
63
+ if (apiKey) {
64
+ // return the global client, we'll initialize it in the useEffect
65
+ return posthogJs
66
+ }
67
+
68
+ console.warn(
69
+ '[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.'
70
+ )
71
+ return posthogJs
72
+ }, [client, apiKey, JSON.stringify(options)]) // Stringify options to be a stable reference
73
+
74
+ // TRICKY: The init needs to happen in a useEffect rather than useMemo, as useEffect does not happen during SSR. Otherwise
75
+ // we'd end up trying to call posthogJs.init() on the server, which can cause issues around hydration and double-init.
76
+ useEffect(() => {
77
+ if (client) {
78
+ // if the user has passed their own client, assume they will also handle calling init().
79
+ return
80
+ }
81
+ const previousInitialization = previousInitializationRef.current
82
+
83
+ if (!previousInitialization) {
84
+ // If it's the first time running this, but it has been loaded elsewhere, warn the user about it.
85
+ if (posthogJs.__loaded) {
86
+ console.warn('[PostHog.js] `posthog` was already loaded elsewhere. This may cause issues.')
87
+ }
88
+
89
+ // Init global client
90
+ posthogJs.init(apiKey, options)
91
+
92
+ // Keep track of whether the client was already initialized
93
+ // This is used to prevent double initialization when running under React.StrictMode, and to know when options change
94
+ previousInitializationRef.current = {
95
+ apiKey: apiKey,
96
+ options: options ?? {},
97
+ }
98
+ } else {
99
+ // If the client was already initialized, we might still end up running the effect again for a few reasons:
100
+ // * someone is developing locally under `React.StrictMode`
101
+ // * the config has changed
102
+ // * the apiKey has changed (not supported!)
103
+ //
104
+ // Changing the apiKey isn't well supported and we'll simply log a message suggesting them
105
+ // to take control of the `client` initialization themselves. This is tricky to handle
106
+ // ourselves because we wouldn't know if we should call `.reset()` or not, for example.
107
+ if (apiKey !== previousInitialization.apiKey) {
108
+ console.warn(
109
+ "[PostHog.js] You have provided a different `apiKey` to `PostHogProvider` than the one that was already initialized. This is not supported by our provider and we'll keep using the previous key. If you need to toggle between API Keys you need to control the `client` yourself and pass it in as a prop rather than an `apiKey` prop."
110
+ )
111
+ }
112
+
113
+ // Changing options is better supported because we can just call `posthogJs.set_config(options)`
114
+ // and they'll be good to go with their new config. The SDK will know how to handle the changes.
115
+ if (options && !isDeepEqual(options, previousInitialization.options)) {
116
+ posthogJs.set_config(options)
117
+ }
118
+
119
+ // Keep track of the possibly-new set of apiKey and options
120
+ previousInitializationRef.current = {
121
+ apiKey: apiKey,
122
+ options: options ?? {},
123
+ }
124
+ }
125
+ }, [client, apiKey, JSON.stringify(options)]) // Stringify options to be a stable reference
126
+
127
+ return <PostHogContext.Provider value={{ client: posthog }}>{children}</PostHogContext.Provider>
128
+ }
@@ -0,0 +1,34 @@
1
+ import * as React from 'react'
2
+ import { render } from '@testing-library/react'
3
+ import { PostHogProvider } from '..'
4
+
5
+ describe('PostHogContext component', () => {
6
+ given(
7
+ 'render',
8
+ () => () =>
9
+ render(
10
+ <PostHogProvider client={given.posthog}>
11
+ <div>Hello</div>
12
+ </PostHogProvider>
13
+ )
14
+ )
15
+ given('posthog', () => ({}))
16
+
17
+ it('should return a client instance from the context if available', () => {
18
+ given.render()
19
+ })
20
+
21
+ it("should not throw error if a client instance can't be found in the context", () => {
22
+ given('posthog', () => undefined) // it might not exist in SSR for example
23
+
24
+ // eslint-disable-next-line no-console
25
+ console.warn = jest.fn()
26
+
27
+ expect(() => given.render()).not.toThrow()
28
+
29
+ // eslint-disable-next-line no-console
30
+ expect(console.warn).toHaveBeenCalledWith(
31
+ '[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.'
32
+ )
33
+ })
34
+ })
@@ -0,0 +1,131 @@
1
+ import * as React from 'react'
2
+ import { render, act } from '@testing-library/react'
3
+ import { PostHogProvider } from '..'
4
+ import posthogJs from 'posthog-js'
5
+
6
+ // Mock posthog-js
7
+ jest.mock('posthog-js', () => ({
8
+ __esModule: true,
9
+ default: {
10
+ init: jest.fn(),
11
+ set_config: jest.fn(),
12
+ __loaded: false,
13
+ },
14
+ }))
15
+
16
+ describe('PostHogProvider component', () => {
17
+ it('should render children components', () => {
18
+ const posthog = {}
19
+ const { getByText } = render(
20
+ <PostHogProvider client={posthog}>
21
+ <div>Test</div>
22
+ </PostHogProvider>
23
+ )
24
+ expect(getByText('Test')).toBeTruthy()
25
+ })
26
+
27
+ describe('when using apiKey initialization', () => {
28
+ const apiKey = 'test-api-key'
29
+ const initialOptions = { api_host: 'https://app.posthog.com' }
30
+ const updatedOptions = { api_host: 'https://eu.posthog.com' }
31
+
32
+ beforeEach(() => {
33
+ jest.clearAllMocks()
34
+ })
35
+
36
+ it('should call set_config when options change', () => {
37
+ const { rerender } = render(
38
+ <PostHogProvider apiKey={apiKey} options={initialOptions}>
39
+ <div>Test</div>
40
+ </PostHogProvider>
41
+ )
42
+
43
+ // First render should initialize
44
+ expect(posthogJs.init).toHaveBeenCalledWith(apiKey, initialOptions)
45
+
46
+ // Rerender with new options
47
+ act(() => {
48
+ rerender(
49
+ <PostHogProvider apiKey={apiKey} options={updatedOptions}>
50
+ <div>Test</div>
51
+ </PostHogProvider>
52
+ )
53
+ })
54
+
55
+ // Should call set_config with new options
56
+ expect(posthogJs.set_config).toHaveBeenCalledWith(updatedOptions)
57
+ })
58
+
59
+ it('should NOT call set_config when we pass new options that are the same as the previous options', () => {
60
+ const { rerender } = render(
61
+ <PostHogProvider apiKey={apiKey} options={initialOptions}>
62
+ <div>Test</div>
63
+ </PostHogProvider>
64
+ )
65
+
66
+ // First render should initialize
67
+ expect(posthogJs.init).toHaveBeenCalledWith(apiKey, initialOptions)
68
+
69
+ // Rerender with new options
70
+ const sameOptionsButDifferentReference = { ...initialOptions }
71
+ act(() => {
72
+ rerender(
73
+ <PostHogProvider apiKey={apiKey} options={sameOptionsButDifferentReference}>
74
+ <div>Test</div>
75
+ </PostHogProvider>
76
+ )
77
+ })
78
+
79
+ // Should NOT call set_config
80
+ expect(posthogJs.set_config).not.toHaveBeenCalled()
81
+ })
82
+
83
+ it('should warn when attempting to change apiKey', () => {
84
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
85
+ const newApiKey = 'different-api-key'
86
+
87
+ const { rerender } = render(
88
+ <PostHogProvider apiKey={apiKey} options={initialOptions}>
89
+ <div>Test</div>
90
+ </PostHogProvider>
91
+ )
92
+
93
+ // First render should initialize
94
+ expect(posthogJs.init).toHaveBeenCalledWith(apiKey, initialOptions)
95
+
96
+ // Rerender with new apiKey
97
+ act(() => {
98
+ rerender(
99
+ <PostHogProvider apiKey={newApiKey} options={initialOptions}>
100
+ <div>Test</div>
101
+ </PostHogProvider>
102
+ )
103
+ })
104
+
105
+ // Should warn about apiKey change
106
+ expect(consoleSpy).toHaveBeenCalledWith(
107
+ expect.stringContaining('You have provided a different `apiKey` to `PostHogProvider`')
108
+ )
109
+
110
+ consoleSpy.mockRestore()
111
+ })
112
+
113
+ it('warns if posthogJs has been loaded elsewhere', () => {
114
+ posthogJs.__loaded = true // Pretend it's initialized
115
+
116
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
117
+ render(
118
+ <PostHogProvider apiKey={apiKey} options={initialOptions}>
119
+ <div>Test</div>
120
+ </PostHogProvider>
121
+ )
122
+
123
+ expect(consoleSpy).toHaveBeenCalledWith(
124
+ expect.stringContaining('`posthog` was already loaded elsewhere. This may cause issues.')
125
+ )
126
+
127
+ consoleSpy.mockRestore()
128
+ posthogJs.__loaded = false
129
+ })
130
+ })
131
+ })
@@ -0,0 +1,2 @@
1
+ export * from './PostHogContext'
2
+ export * from './PostHogProvider'
@@ -0,0 +1,15 @@
1
+ import type { ErrorInfo } from 'react'
2
+ import { PostHog } from '../context'
3
+ import { CaptureResult } from 'posthog-js'
4
+
5
+ export const setupReactErrorHandler = (
6
+ client: PostHog,
7
+ callback?: (event: CaptureResult | undefined, error: any, errorInfo: ErrorInfo) => void
8
+ ) => {
9
+ return (error: any, errorInfo: ErrorInfo): void => {
10
+ const event = client.captureException(error)
11
+ if (callback) {
12
+ callback(event, error, errorInfo)
13
+ }
14
+ }
15
+ }
@@ -0,0 +1 @@
1
+ export * from './error-helpers'
@@ -0,0 +1,93 @@
1
+ import * as React from 'react'
2
+ import { renderHook } from '@testing-library/react-hooks'
3
+ import { PostHogProvider } from '../../context'
4
+ import { useFeatureFlagPayload, useFeatureFlagVariantKey, useFeatureFlagEnabled, useActiveFeatureFlags } from '../index'
5
+
6
+ jest.useFakeTimers()
7
+
8
+ const ACTIVE_FEATURE_FLAGS = ['example_feature_true', 'multivariate_feature', 'example_feature_payload']
9
+
10
+ const FEATURE_FLAG_STATUS = {
11
+ example_feature_true: true,
12
+ example_feature_false: false,
13
+ multivariate_feature: 'string-value',
14
+ example_feature_payload: 'test',
15
+ }
16
+
17
+ const FEATURE_FLAG_PAYLOADS = {
18
+ example_feature_payload: {
19
+ id: 1,
20
+ name: 'example_feature_1_payload',
21
+ key: 'example_feature_1_payload',
22
+ },
23
+ }
24
+
25
+ describe('useFeatureFlagPayload hook', () => {
26
+ given('renderProvider', () => ({ children }) => (
27
+ <PostHogProvider client={given.posthog}>{children}</PostHogProvider>
28
+ ))
29
+
30
+ given('posthog', () => ({
31
+ isFeatureEnabled: (flag) => !!FEATURE_FLAG_STATUS[flag],
32
+ getFeatureFlag: (flag) => FEATURE_FLAG_STATUS[flag],
33
+ getFeatureFlagPayload: (flag) => FEATURE_FLAG_PAYLOADS[flag],
34
+ onFeatureFlags: (callback) => {
35
+ const activeFlags = []
36
+ for (const flag in FEATURE_FLAG_STATUS) {
37
+ if (FEATURE_FLAG_STATUS[flag]) {
38
+ activeFlags.push(flag)
39
+ }
40
+ }
41
+ callback(activeFlags)
42
+ return () => {}
43
+ },
44
+ featureFlags: {
45
+ getFlags: () => ACTIVE_FEATURE_FLAGS,
46
+ },
47
+ }))
48
+
49
+ it.each([
50
+ ['example_feature_true', true],
51
+ ['example_feature_false', false],
52
+ ['missing', false],
53
+ ['multivariate_feature', true],
54
+ ['example_feature_payload', true],
55
+ ])('should get the boolean feature flag', (flag, expected) => {
56
+ let { result } = renderHook(() => useFeatureFlagEnabled(flag), {
57
+ wrapper: given.renderProvider,
58
+ })
59
+ expect(result.current).toEqual(expected)
60
+ })
61
+
62
+ it.each([
63
+ ['example_feature_true', undefined],
64
+ ['example_feature_false', undefined],
65
+ ['missing', undefined],
66
+ ['multivariate_feature', undefined],
67
+ ['example_feature_payload', FEATURE_FLAG_PAYLOADS.example_feature_payload],
68
+ ])('should get the payload feature flag', (flag, expected) => {
69
+ let { result } = renderHook(() => useFeatureFlagPayload(flag), {
70
+ wrapper: given.renderProvider,
71
+ })
72
+ expect(result.current).toEqual(expected)
73
+ })
74
+
75
+ it('should return the active feature flags', () => {
76
+ let { result } = renderHook(() => useActiveFeatureFlags(), {
77
+ wrapper: given.renderProvider,
78
+ })
79
+ expect(result.current).toEqual(['example_feature_true', 'multivariate_feature', 'example_feature_payload'])
80
+ })
81
+
82
+ it.each([
83
+ ['example_feature_true', true],
84
+ ['example_feature_false', false],
85
+ ['missing', undefined],
86
+ ['multivariate_feature', 'string-value'],
87
+ ])('should get the feature flag variant key', (flag, expected) => {
88
+ let { result } = renderHook(() => useFeatureFlagVariantKey(flag), {
89
+ wrapper: given.renderProvider,
90
+ })
91
+ expect(result.current).toEqual(expected)
92
+ })
93
+ })
@@ -0,0 +1,23 @@
1
+ import * as React from 'react'
2
+ import { renderHook } from '@testing-library/react-hooks'
3
+ import { PostHogProvider } from '../../context'
4
+ import { usePostHog } from '..'
5
+
6
+ jest.useFakeTimers()
7
+
8
+ const posthog = { posthog_client: true }
9
+
10
+ describe('usePostHog hook', () => {
11
+ given('renderProvider', () => ({ children }) => (
12
+ <PostHogProvider client={given.posthog}>{children}</PostHogProvider>
13
+ ))
14
+
15
+ given('posthog', () => posthog)
16
+
17
+ it('should return the client', () => {
18
+ let { result } = renderHook(() => usePostHog(), {
19
+ wrapper: given.renderProvider,
20
+ })
21
+ expect(result.current).toEqual(posthog)
22
+ })
23
+ })
@@ -0,0 +1,5 @@
1
+ export * from './useFeatureFlagEnabled'
2
+ export * from './useFeatureFlagPayload'
3
+ export * from './useActiveFeatureFlags'
4
+ export * from './useFeatureFlagVariantKey'
5
+ export * from './usePostHog'
@@ -0,0 +1,16 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { usePostHog } from './usePostHog'
3
+
4
+ export function useActiveFeatureFlags(): string[] {
5
+ const client = usePostHog()
6
+
7
+ const [featureFlags, setFeatureFlags] = useState<string[]>(() => client.featureFlags.getFlags())
8
+
9
+ useEffect(() => {
10
+ return client.onFeatureFlags((flags) => {
11
+ setFeatureFlags(flags)
12
+ })
13
+ }, [client])
14
+
15
+ return featureFlags
16
+ }
@@ -0,0 +1,16 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { usePostHog } from './usePostHog'
3
+
4
+ export function useFeatureFlagEnabled(flag: string): boolean | undefined {
5
+ const client = usePostHog()
6
+
7
+ const [featureEnabled, setFeatureEnabled] = useState<boolean | undefined>(() => client.isFeatureEnabled(flag))
8
+
9
+ useEffect(() => {
10
+ return client.onFeatureFlags(() => {
11
+ setFeatureEnabled(client.isFeatureEnabled(flag))
12
+ })
13
+ }, [client, flag])
14
+
15
+ return featureEnabled
16
+ }
@@ -0,0 +1,17 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { JsonType } from 'posthog-js'
3
+ import { usePostHog } from './usePostHog'
4
+
5
+ export function useFeatureFlagPayload(flag: string): JsonType {
6
+ const client = usePostHog()
7
+
8
+ const [featureFlagPayload, setFeatureFlagPayload] = useState<JsonType>(() => client.getFeatureFlagPayload(flag))
9
+
10
+ useEffect(() => {
11
+ return client.onFeatureFlags(() => {
12
+ setFeatureFlagPayload(client.getFeatureFlagPayload(flag))
13
+ })
14
+ }, [client, flag])
15
+
16
+ return featureFlagPayload
17
+ }
@@ -0,0 +1,18 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { usePostHog } from './usePostHog'
3
+
4
+ export function useFeatureFlagVariantKey(flag: string): string | boolean | undefined {
5
+ const client = usePostHog()
6
+
7
+ const [featureFlagVariantKey, setFeatureFlagVariantKey] = useState<string | boolean | undefined>(() =>
8
+ client.getFeatureFlag(flag)
9
+ )
10
+
11
+ useEffect(() => {
12
+ return client.onFeatureFlags(() => {
13
+ setFeatureFlagVariantKey(client.getFeatureFlag(flag))
14
+ })
15
+ }, [client, flag])
16
+
17
+ return featureFlagVariantKey
18
+ }
@@ -0,0 +1,7 @@
1
+ import { useContext } from 'react'
2
+ import { PostHog, PostHogContext } from '../context'
3
+
4
+ export const usePostHog = (): PostHog => {
5
+ const { client } = useContext(PostHogContext)
6
+ return client
7
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './context'
2
+ export * from './hooks'
3
+ export * from './components'
4
+ export * from './helpers'
@@ -0,0 +1,42 @@
1
+ import { isDeepEqual } from '../object-utils'
2
+
3
+ const circularArray1: any[] = []
4
+ circularArray1.push(circularArray1)
5
+ const circularArray2: any[] = []
6
+ circularArray2.push(circularArray2)
7
+
8
+ function f1() {}
9
+ function f2() {}
10
+
11
+ describe('object-utils', () => {
12
+ describe('isDeepEqual', () => {
13
+ it.each([
14
+ [true, { a: 1, b: 2 }, { a: 1, b: 2 }],
15
+ [true, { a: 1, b: { c: 2 } }, { a: 1, b: { c: 2 } }],
16
+ [false, { a: 1, b: 2 }, { a: 1, b: 3 }],
17
+ [false, { a: 1, b: 2 }, { a: 1 }],
18
+ [true, 'a', 'a'],
19
+ [false, 'a', 'b'],
20
+ [false, 1, 2],
21
+ [true, 0, -0],
22
+ [false, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY],
23
+ [false, 1, '1'],
24
+ [false, Number.NaN, Number.NaN],
25
+ [true, null, null],
26
+ [false, undefined, null],
27
+ [true, [], []],
28
+ [true, [[[[]]]], [[[[]]]]],
29
+ [false, [[[[]]]], [[[[[]]]]]],
30
+ [true, [1, 2, 3], [1, 2, 3]],
31
+ [false, [1, 2, 3], [1, 2, 4]],
32
+ [true, { a: circularArray1 }, { a: circularArray1 }],
33
+ [true, { a: circularArray1 }, { a: circularArray2 }],
34
+ [true, circularArray1, [circularArray1]],
35
+ [true, f1, f1],
36
+ [false, f1, f2],
37
+ ])('returns %s for %s and %s', (expected, obj1, obj2) => {
38
+ expect(isDeepEqual(obj1, obj2)).toBe(expected)
39
+ expect(isDeepEqual(obj2, obj1)).toBe(expected)
40
+ })
41
+ })
42
+ })
@@ -0,0 +1,36 @@
1
+ // Deeply compares two objects for equality.
2
+ // Use a WeakMap to keep track of visited objects to avoid infinite recursion.
3
+ // WeakMap is supported in IE11, see https://caniuse.com/?search=JavaScript%20WeakMap
4
+
5
+ export function isDeepEqual(obj1: any, obj2: any, visited = new WeakMap()): boolean {
6
+ if (obj1 === obj2) {
7
+ return true
8
+ }
9
+
10
+ if (typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) {
11
+ return false
12
+ }
13
+
14
+ if (visited.has(obj1) && visited.get(obj1) === obj2) {
15
+ return true
16
+ }
17
+ visited.set(obj1, obj2)
18
+
19
+ const keys1 = Object.keys(obj1)
20
+ const keys2 = Object.keys(obj2)
21
+
22
+ if (keys1.length !== keys2.length) {
23
+ return false
24
+ }
25
+
26
+ for (const key of keys1) {
27
+ if (!keys2.includes(key)) {
28
+ return false
29
+ }
30
+ if (!isDeepEqual(obj1[key], obj2[key], visited)) {
31
+ return false
32
+ }
33
+ }
34
+
35
+ return true
36
+ }
@@ -0,0 +1,16 @@
1
+ // from a comment on http://dbj.org/dbj/?p=286
2
+ // fails on only one very rare and deliberate custom object:
3
+ // let bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }};
4
+ export const isFunction = function (f: any): f is (...args: any[]) => any {
5
+ // eslint-disable-next-line posthog-js/no-direct-function-check
6
+ return typeof f === 'function'
7
+ }
8
+
9
+ export const isUndefined = function (x: unknown): x is undefined {
10
+ return x === void 0
11
+ }
12
+
13
+ export const isNull = function (x: unknown): x is null {
14
+ // eslint-disable-next-line posthog-js/no-direct-null-check
15
+ return x === null
16
+ }