@hanzo/insights-react 1.8.1
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.
- package/LICENSE +21 -0
- package/README.md +198 -0
- package/dist/esm/index.js +426 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/surveys/index.js +98 -0
- package/dist/esm/surveys/index.js.map +1 -0
- package/dist/types/index.d.ts +96 -0
- package/dist/types/surveys/index.d.ts +19 -0
- package/dist/umd/index.js +449 -0
- package/dist/umd/index.js.map +1 -0
- package/dist/umd/surveys/index.js +107 -0
- package/dist/umd/surveys/index.js.map +1 -0
- package/package.json +64 -0
- package/src/components/PostHogCaptureOnViewed.tsx +126 -0
- package/src/components/PostHogErrorBoundary.tsx +89 -0
- package/src/components/PostHogFeature.tsx +91 -0
- package/src/components/__tests__/PostHogCaptureOnViewed.test.tsx +110 -0
- package/src/components/__tests__/PostHogErrorBoundary.test.tsx +110 -0
- package/src/components/__tests__/PostHogFeature.test.tsx +291 -0
- package/src/components/index.ts +7 -0
- package/src/components/internal/VisibilityAndClickTracker.tsx +49 -0
- package/src/components/internal/VisibilityAndClickTrackers.tsx +60 -0
- package/src/context/PostHogContext.ts +9 -0
- package/src/context/PostHogProvider.tsx +136 -0
- package/src/context/__tests__/PostHogContext.test.tsx +35 -0
- package/src/context/__tests__/PostHogProvider.test.tsx +131 -0
- package/src/context/index.ts +2 -0
- package/src/helpers/error-helpers.ts +15 -0
- package/src/helpers/index.ts +1 -0
- package/src/hooks/__tests__/featureFlags.test.tsx +273 -0
- package/src/hooks/__tests__/usePostHog.test.tsx +19 -0
- package/src/hooks/__tests__/useThumbSurvey.test.tsx +105 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useActiveFeatureFlags.ts +21 -0
- package/src/hooks/useFeatureFlagEnabled.ts +24 -0
- package/src/hooks/useFeatureFlagPayload.ts +22 -0
- package/src/hooks/useFeatureFlagResult.ts +31 -0
- package/src/hooks/useFeatureFlagVariantKey.ts +22 -0
- package/src/hooks/usePostHog.ts +7 -0
- package/src/hooks/useThumbSurvey.ts +146 -0
- package/src/index.ts +4 -0
- package/src/surveys/index.ts +1 -0
- package/src/utils/__tests__/object-utils.test.ts +42 -0
- package/src/utils/object-utils.ts +36 -0
- package/src/utils/type-utils.ts +16 -0
- package/surveys/package.json +7 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useContext, useEffect, useState } from 'react'
|
|
2
|
+
import { PostHogContext } from '../context'
|
|
3
|
+
|
|
4
|
+
export function useFeatureFlagVariantKey(flag: string): string | boolean | undefined {
|
|
5
|
+
const { client, bootstrap } = useContext(PostHogContext)
|
|
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
|
+
if (!client?.featureFlags?.hasLoadedFlags && bootstrap?.featureFlags) {
|
|
18
|
+
return bootstrap.featureFlags[flag]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return featureFlagVariantKey
|
|
22
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useMemo, type RefCallback, useEffect } from 'react'
|
|
2
|
+
import { usePostHog } from './usePostHog'
|
|
3
|
+
import { DisplaySurveyType, SurveyEventName, SurveyEventProperties, SurveyPosition } from '@hanzo/insights'
|
|
4
|
+
|
|
5
|
+
export interface UseThumbSurveyOptions {
|
|
6
|
+
/** ID of the target PostHog survey */
|
|
7
|
+
surveyId: string
|
|
8
|
+
/** Configure the position of the pop-up for followup questions, if applicable. Defaults to SurveyPosition.NextToTrigger */
|
|
9
|
+
displayPosition?: SurveyPosition
|
|
10
|
+
/** Custom event properties to pass with each survey result */
|
|
11
|
+
properties?: Record<string, any>
|
|
12
|
+
/** Callback on thumb button click */
|
|
13
|
+
onResponse?: (response: 'up' | 'down') => void
|
|
14
|
+
/** Disable automatically emitting `survey shown` on hook mount. Defaults to false. */
|
|
15
|
+
disableAutoShownTracking?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UseThumbSurveyResult {
|
|
19
|
+
/** Call this to submit a survey response, with value 'up' or 'down' */
|
|
20
|
+
respond: (value: 'up' | 'down') => void
|
|
21
|
+
/** User's response value, available after submission */
|
|
22
|
+
response: 'up' | 'down' | null
|
|
23
|
+
/** Ref to attach to the trigger element for positioning the followup survey popup */
|
|
24
|
+
triggerRef: RefCallback<HTMLElement>
|
|
25
|
+
/** Method to manually trigger a `survey shown` event. Only available when disableAutoShownTracking is true. */
|
|
26
|
+
trackShown?: () => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const TRIGGER_ATTR = 'data-ph-thumb-survey-trigger'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Convenience hook for managing a "thumb" (1-2 rating scale) survey.
|
|
33
|
+
*
|
|
34
|
+
* Pre-requisites:
|
|
35
|
+
* 1) Ensure surveys are not disabled in your PostHog config (`disable_surveys: false`)
|
|
36
|
+
* 2) Ensure surveys are enabled in your PostHog project (Settings > Surveys > Enable surveys)
|
|
37
|
+
*
|
|
38
|
+
* How-to:
|
|
39
|
+
* 1) Create an API survey in PostHog (New survey > Presentation > API)
|
|
40
|
+
* 2) Set the first question to a thumb rating scale (Question type: Rating -> Display type: Emoji -> Scale: 1-2 (thumbs up/down))
|
|
41
|
+
* 3) Set the thumb question to "Automatically submit on selection"
|
|
42
|
+
* 4) Optionally add follow-up questions
|
|
43
|
+
* 5) Use the hook:
|
|
44
|
+
*
|
|
45
|
+
* ```typescript
|
|
46
|
+
* const { respond, response, triggerRef } = useThumbSurvey({
|
|
47
|
+
* surveyId: 'my-survey-id',
|
|
48
|
+
* properties: { foo: 'bar' }, // optional custom properties to pass along with the survey responses
|
|
49
|
+
* onResponse: (response) => { console.log(response) }, // optional callback on submission
|
|
50
|
+
* })
|
|
51
|
+
*
|
|
52
|
+
* return (
|
|
53
|
+
* <div ref={triggerRef}>
|
|
54
|
+
* <button onClick={() => respond('up')} style={{ color: response === 'up' ? 'green' : undefined }}>👍</button>
|
|
55
|
+
* <button onClick={() => respond('down')} style={{ color: response === 'down' ? 'red' : undefined }}>👎</button>
|
|
56
|
+
* </div>
|
|
57
|
+
* )
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* 6) If your survey has further questions, the survey will automatically display as a popover, either:
|
|
61
|
+
* - [default] next to the triggerRef element,
|
|
62
|
+
* - OR wherever you specify in options.position
|
|
63
|
+
*
|
|
64
|
+
* Notes:
|
|
65
|
+
* - The thumbs up/down response will ALWAYS be recorded, whether your survey is set to collect partial responses or not.
|
|
66
|
+
* - By default, followup questions will be displayed as a pop-up next to the triggerRef. Use options.position to change the position.
|
|
67
|
+
* - By default, `survey shown` is emitted automatically on hook mount. To prevent this behavior, set `disableAutoShownTracking: true`,
|
|
68
|
+
* and manually call `trackShown()` when you want to emit this event.
|
|
69
|
+
*
|
|
70
|
+
* @param options UseThumbSurveyOptions
|
|
71
|
+
* @returns UseThumbSurveyResult
|
|
72
|
+
*/
|
|
73
|
+
export function useThumbSurvey({
|
|
74
|
+
surveyId,
|
|
75
|
+
displayPosition = SurveyPosition.NextToTrigger,
|
|
76
|
+
properties,
|
|
77
|
+
onResponse,
|
|
78
|
+
disableAutoShownTracking,
|
|
79
|
+
}: UseThumbSurveyOptions): UseThumbSurveyResult {
|
|
80
|
+
const posthog = usePostHog()
|
|
81
|
+
const [responded, setResponded] = useState<'up' | 'down' | null>(null)
|
|
82
|
+
const [instanceId] = useState(() => Math.random().toString(36).slice(2, 9))
|
|
83
|
+
const triggerValue = useMemo(() => `${surveyId}-${instanceId}`, [surveyId, instanceId])
|
|
84
|
+
|
|
85
|
+
const elementRef = useRef<HTMLElement | null>(null)
|
|
86
|
+
const triggerRef = useCallback(
|
|
87
|
+
(el: HTMLElement | null) => {
|
|
88
|
+
if (elementRef.current) {
|
|
89
|
+
elementRef.current.removeAttribute(TRIGGER_ATTR)
|
|
90
|
+
}
|
|
91
|
+
elementRef.current = el
|
|
92
|
+
if (el) {
|
|
93
|
+
el.setAttribute(TRIGGER_ATTR, triggerValue)
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
[triggerValue]
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const shownRef = useRef(false)
|
|
100
|
+
const respondedRef = useRef(false)
|
|
101
|
+
|
|
102
|
+
const trackShown = useCallback(() => {
|
|
103
|
+
if (shownRef.current || !posthog) return
|
|
104
|
+
shownRef.current = true
|
|
105
|
+
posthog.capture(SurveyEventName.SHOWN, {
|
|
106
|
+
[SurveyEventProperties.SURVEY_ID]: surveyId,
|
|
107
|
+
sessionRecordingUrl: posthog.get_session_replay_url?.(),
|
|
108
|
+
...properties,
|
|
109
|
+
})
|
|
110
|
+
}, [posthog, surveyId, properties])
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (!disableAutoShownTracking) {
|
|
114
|
+
trackShown()
|
|
115
|
+
}
|
|
116
|
+
}, [trackShown, disableAutoShownTracking])
|
|
117
|
+
|
|
118
|
+
const respond = useCallback(
|
|
119
|
+
(value: 'up' | 'down') => {
|
|
120
|
+
if (!posthog?.surveys || respondedRef.current) return
|
|
121
|
+
respondedRef.current = true
|
|
122
|
+
|
|
123
|
+
setResponded(value)
|
|
124
|
+
onResponse?.(value)
|
|
125
|
+
|
|
126
|
+
posthog.surveys.displaySurvey(surveyId, {
|
|
127
|
+
displayType: DisplaySurveyType.Popover,
|
|
128
|
+
ignoreConditions: true,
|
|
129
|
+
ignoreDelay: true,
|
|
130
|
+
properties,
|
|
131
|
+
initialResponses: { 0: value === 'up' ? 1 : 2 },
|
|
132
|
+
position: displayPosition,
|
|
133
|
+
selector: `[${TRIGGER_ATTR}="${triggerValue}"]`,
|
|
134
|
+
skipShownEvent: true,
|
|
135
|
+
})
|
|
136
|
+
},
|
|
137
|
+
[posthog, surveyId, displayPosition, properties, onResponse, triggerValue]
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
respond,
|
|
142
|
+
response: responded,
|
|
143
|
+
triggerRef,
|
|
144
|
+
...(disableAutoShownTracking && { trackShown }),
|
|
145
|
+
}
|
|
146
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../hooks/useThumbSurvey'
|
|
@@ -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
|
+
}
|