@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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +198 -0
  3. package/dist/esm/index.js +426 -0
  4. package/dist/esm/index.js.map +1 -0
  5. package/dist/esm/surveys/index.js +98 -0
  6. package/dist/esm/surveys/index.js.map +1 -0
  7. package/dist/types/index.d.ts +96 -0
  8. package/dist/types/surveys/index.d.ts +19 -0
  9. package/dist/umd/index.js +449 -0
  10. package/dist/umd/index.js.map +1 -0
  11. package/dist/umd/surveys/index.js +107 -0
  12. package/dist/umd/surveys/index.js.map +1 -0
  13. package/package.json +64 -0
  14. package/src/components/PostHogCaptureOnViewed.tsx +126 -0
  15. package/src/components/PostHogErrorBoundary.tsx +89 -0
  16. package/src/components/PostHogFeature.tsx +91 -0
  17. package/src/components/__tests__/PostHogCaptureOnViewed.test.tsx +110 -0
  18. package/src/components/__tests__/PostHogErrorBoundary.test.tsx +110 -0
  19. package/src/components/__tests__/PostHogFeature.test.tsx +291 -0
  20. package/src/components/index.ts +7 -0
  21. package/src/components/internal/VisibilityAndClickTracker.tsx +49 -0
  22. package/src/components/internal/VisibilityAndClickTrackers.tsx +60 -0
  23. package/src/context/PostHogContext.ts +9 -0
  24. package/src/context/PostHogProvider.tsx +136 -0
  25. package/src/context/__tests__/PostHogContext.test.tsx +35 -0
  26. package/src/context/__tests__/PostHogProvider.test.tsx +131 -0
  27. package/src/context/index.ts +2 -0
  28. package/src/helpers/error-helpers.ts +15 -0
  29. package/src/helpers/index.ts +1 -0
  30. package/src/hooks/__tests__/featureFlags.test.tsx +273 -0
  31. package/src/hooks/__tests__/usePostHog.test.tsx +19 -0
  32. package/src/hooks/__tests__/useThumbSurvey.test.tsx +105 -0
  33. package/src/hooks/index.ts +6 -0
  34. package/src/hooks/useActiveFeatureFlags.ts +21 -0
  35. package/src/hooks/useFeatureFlagEnabled.ts +24 -0
  36. package/src/hooks/useFeatureFlagPayload.ts +22 -0
  37. package/src/hooks/useFeatureFlagResult.ts +31 -0
  38. package/src/hooks/useFeatureFlagVariantKey.ts +22 -0
  39. package/src/hooks/usePostHog.ts +7 -0
  40. package/src/hooks/useThumbSurvey.ts +146 -0
  41. package/src/index.ts +4 -0
  42. package/src/surveys/index.ts +1 -0
  43. package/src/utils/__tests__/object-utils.test.ts +42 -0
  44. package/src/utils/object-utils.ts +36 -0
  45. package/src/utils/type-utils.ts +16 -0
  46. 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,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
+ }
@@ -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,4 @@
1
+ export * from './context'
2
+ export * from './hooks'
3
+ export * from './components'
4
+ export * from './helpers'
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "@posthog/react-surveys",
3
+ "private": true,
4
+ "main": "../dist/umd/surveys/index.js",
5
+ "module": "../dist/esm/surveys/index.js",
6
+ "types": "../dist/types/surveys/index.d.ts"
7
+ }