Skip to content

Commit 41c41cf

Browse files
razor-xseambot
andauthored
feat: Add telemetry (#458)
Co-authored-by: Seam Bot <[email protected]>
1 parent ee89a0c commit 41c41cf

File tree

25 files changed

+4594
-2041
lines changed

25 files changed

+4594
-2041
lines changed

README.md

+15
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,21 @@ When serving the CSS styles from the default CDN, extend `style-src` with
246246
style-src https://react.seam.co
247247
```
248248

249+
### Telemetry
250+
251+
By default, this library reports basic usage telemetry to the Seam API.
252+
This may be completely disabled with `<SeamProvider disableTelemetry>`.
253+
254+
Before disabling telemetry, please consider the following:
255+
256+
- Telemetry is sent directly to the Seam API and is never sold to third parties.
257+
- Telemetry is used by Seam for the sole purpose of improving Seam Components
258+
and directly enhancing the experience for your end users.
259+
- No data is persisted on the client beyond the lifetime of the browser session: this library does not use cookies or local browser storage.
260+
- Telemetry may be selectively disabled for some users to align with any existing data collection compliance requirements of your application.
261+
- Telemetry does not negatively impact performance and adds minimal background network overhead.
262+
- The implementation is small, simple to audit, and completely transparent: [src/lib/telemetry](src/lib/telemetry).
263+
249264
## Development and Testing
250265

251266
### Quickstart

package-lock.json

+4,104-2,027
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -124,16 +124,17 @@
124124
"@tanstack/react-query": "^4.28.0",
125125
"classnames": "^2.3.2",
126126
"luxon": "^3.3.0",
127+
"queue": "^7.0.0",
127128
"react-hook-form": "^7.46.1",
128-
"seamapi": "^8.11.0",
129+
"seamapi": "^8.12.0",
129130
"uuid": "^9.0.0"
130131
},
131132
"devDependencies": {
132133
"@emotion/styled": "^11.10.6",
133134
"@mui/icons-material": "^5.11.16",
134135
"@mui/material": "^5.12.2",
135136
"@rxfork/r2wc-react-to-web-component": "^2.3.0",
136-
"@seamapi/fake-seam-connect": "^1.11.0",
137+
"@seamapi/fake-seam-connect": "^1.14.0",
137138
"@storybook/addon-designs": "^7.0.1",
138139
"@storybook/addon-essentials": "^7.0.2",
139140
"@storybook/addon-links": "^7.0.2",

src/elements.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
declare const elementNames: string[]
1+
export const elementNames: string[]
22
export default elementNames

src/lib/element.tsx

+14-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
} from 'lib/seam/SeamProvider.js'
1111

1212
declare global {
13+
// eslint-disable-next-line no-var
14+
var disableSeamTelemetry: boolean | undefined
1315
// eslint-disable-next-line no-var
1416
var disableSeamCssInjection: boolean | undefined
1517
// eslint-disable-next-line no-var
@@ -51,6 +53,8 @@ const providerProps: R2wcProps<ProviderProps> = {
5153
clientSessionToken: 'string',
5254
endpoint: 'string',
5355
queryClient: 'object',
56+
telemetryClient: 'object',
57+
disableTelemetry: 'boolean',
5458
disableCssInjection: 'boolean',
5559
disableFontInjection: 'boolean',
5660
unminifiyCss: 'boolean',
@@ -73,12 +77,15 @@ export const defineCustomElement = ({
7377

7478
function withProvider<P extends JSX.IntrinsicAttributes>(
7579
Component: ComponentType<P>
76-
) {
77-
return function ({
80+
): (props: ProviderProps & { container: Container } & P) => JSX.Element | null {
81+
const name = Component.displayName ?? Component.name ?? 'Component'
82+
83+
function WithProvider({
7884
publishableKey,
7985
endpoint,
8086
userIdentifierKey,
8187
clientSessionToken,
88+
disableTelemetry,
8289
disableCssInjection,
8390
disableFontInjection,
8491
unminifiyCss,
@@ -91,6 +98,7 @@ function withProvider<P extends JSX.IntrinsicAttributes>(
9198
userIdentifierKey={userIdentifierKey}
9299
clientSessionToken={clientSessionToken}
93100
endpoint={endpoint}
101+
disableTelemetry={disableTelemetry ?? globalThis.disableSeamTelemetry}
94102
disableCssInjection={
95103
disableCssInjection ?? globalThis.disableSeamCssInjection
96104
}
@@ -103,4 +111,8 @@ function withProvider<P extends JSX.IntrinsicAttributes>(
103111
</SeamProvider>
104112
)
105113
}
114+
115+
WithProvider.displayName = `WithProvider(${name})`
116+
117+
return WithProvider
106118
}

src/lib/seam/SeamProvider.tsx

+33-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ import {
77
} from 'react'
88
import type { Seam, SeamClientOptions } from 'seamapi'
99

10+
import {
11+
type TelemetryClient,
12+
TelemetryProvider,
13+
useUserTelemetry,
14+
} from 'lib/telemetry/index.js'
15+
1016
import { useSeamFont } from 'lib/seam/use-seam-font.js'
1117
import { useSeamStyles } from 'lib/seam/use-seam-styles.js'
1218

@@ -15,6 +21,8 @@ declare global {
1521
var seam: SeamProviderProps | undefined
1622
// eslint-disable-next-line no-var
1723
var seamQueryClient: QueryClient | undefined
24+
// eslint-disable-next-line no-var
25+
var seamTelemetryClient: TelemetryClient | undefined
1826
}
1927

2028
export interface SeamContext {
@@ -48,10 +56,12 @@ export interface SeamProviderPropsWithClientSessionToken
4856
}
4957

5058
interface SeamProviderBaseProps extends PropsWithChildren {
59+
disableTelemetry?: boolean | undefined
5160
disableCssInjection?: boolean | undefined
5261
disableFontInjection?: boolean | undefined
5362
unminifiyCss?: boolean | undefined
5463
queryClient?: QueryClient | undefined
64+
telemetryClient?: TelemetryClient | undefined
5565
}
5666

5767
export type SeamProviderClientOptions = Pick<SeamClientOptions, 'endpoint'>
@@ -60,10 +70,12 @@ const defaultQueryClient = new QueryClient()
6070

6171
export function SeamProvider({
6272
children,
73+
disableTelemetry = false,
6374
disableCssInjection = false,
6475
disableFontInjection = false,
6576
unminifiyCss = false,
6677
queryClient,
78+
telemetryClient,
6779
...props
6880
}: SeamProviderProps): JSX.Element {
6981
useSeamStyles({ disabled: disableCssInjection, unminified: unminifiyCss })
@@ -93,17 +105,34 @@ export function SeamProvider({
93105

94106
const { Provider } = seamContext
95107

108+
const endpoint = 'endpoint' in props ? props.endpoint : undefined
109+
96110
return (
97111
<div className='seam-components'>
98-
<QueryClientProvider
99-
client={queryClient ?? globalThis.seamQueryClient ?? defaultQueryClient}
112+
<TelemetryProvider
113+
client={telemetryClient ?? globalThis.seamTelemetryClient}
114+
disabled={disableTelemetry}
115+
endpoint={endpoint}
100116
>
101-
<Provider value={value}>{children}</Provider>
102-
</QueryClientProvider>
117+
<QueryClientProvider
118+
client={
119+
queryClient ?? globalThis.seamQueryClient ?? defaultQueryClient
120+
}
121+
>
122+
<Provider value={value}>
123+
<Wrapper>{children}</Wrapper>
124+
</Provider>
125+
</QueryClientProvider>
126+
</TelemetryProvider>
103127
</div>
104128
)
105129
}
106130

131+
function Wrapper({ children }: PropsWithChildren): JSX.Element | null {
132+
useUserTelemetry()
133+
return <>{children}</>
134+
}
135+
107136
const createDefaultSeamContextValue = (): SeamContext => {
108137
try {
109138
if (globalThis.seam == null) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { useQuery } from '@tanstack/react-query'
2+
import type {
3+
ClientSession,
4+
ClientSessionsGetResponse,
5+
SeamError,
6+
} from 'seamapi'
7+
8+
import { useSeamClient } from 'lib/seam/use-seam-client.js'
9+
import type { UseSeamQueryResult } from 'lib/seam/use-seam-query-result.js'
10+
11+
export type UseClientSessionParams = never
12+
export type UseClientSessionData = ClientSession | null
13+
14+
export function useClientSession(): UseSeamQueryResult<
15+
'clientSession',
16+
UseClientSessionData
17+
> {
18+
const { client } = useSeamClient()
19+
const { data, ...rest } = useQuery<
20+
ClientSessionsGetResponse['client_session'] | null,
21+
SeamError
22+
>({
23+
enabled: client != null,
24+
queryKey: ['client_session', 'get'],
25+
queryFn: async () => {
26+
if (client == null) return null
27+
return await client.clientSessions.get({})
28+
},
29+
})
30+
31+
return { ...rest, clientSession: data }
32+
}

src/lib/seam/components/AccessCodeDetails/AccessCodeDetails.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import type {
88
DeviceError,
99
} from 'seamapi'
1010

11+
import { useComponentTelemetry } from 'lib/telemetry/index.js'
12+
1113
import { CopyIcon } from 'lib/icons/Copy.js'
1214
import { useAccessCode } from 'lib/seam/access-codes/use-access-code.js'
1315
import { useDeleteAccessCode } from 'lib/seam/access-codes/use-delete-access-code.js'
@@ -42,6 +44,8 @@ export function AccessCodeDetails({
4244
onBack,
4345
className,
4446
}: AccessCodeDetailsProps): JSX.Element | null {
47+
useComponentTelemetry('AccessCodeDetails')
48+
4549
const { accessCode } = useAccessCode(accessCodeId)
4650
const [selectedDeviceId, selectDevice] = useState<string | null>(null)
4751
const { mutate: deleteCode, isLoading: isDeleting } = useDeleteAccessCode()

src/lib/seam/components/AccessCodeTable/AccessCodeTable.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import classNames from 'classnames'
22
import { useCallback, useMemo, useState } from 'react'
33

4+
import { useComponentTelemetry } from 'lib/telemetry/index.js'
5+
46
import { compareByCreatedAtDesc } from 'lib/dates.js'
57
import { AddIcon } from 'lib/icons/Add.js'
68
import {
@@ -83,6 +85,8 @@ export function AccessCodeTable({
8385
disableLockUnlock = false,
8486
disableDeleteAccessCode = false,
8587
}: AccessCodeTableProps): JSX.Element {
88+
useComponentTelemetry('AccessCodeTable')
89+
8690
const { accessCodes } = useAccessCodes({
8791
device_id: deviceId,
8892
})

src/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDetails.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import classNames from 'classnames'
22
import { useState } from 'react'
33

4+
import { useComponentTelemetry } from 'lib/telemetry/index.js'
5+
46
import { formatDateAndTime } from 'lib/dates.js'
57
import { ArrowRightIcon } from 'lib/icons/ArrowRight.js'
68
import { ClimateSettingScheduleCard } from 'lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleCard.js'
@@ -33,6 +35,8 @@ export function ClimateSettingScheduleDetails({
3335
disableCreateAccessCode,
3436
disableEditAccessCode,
3537
}: ClimateSettingScheduleDetailsProps): JSX.Element | null {
38+
useComponentTelemetry('ClimateSettingScheduleDetails')
39+
3640
const { climateSettingSchedule } = useClimateSettingSchedule(
3741
climateSettingScheduleId
3842
)

src/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleTable.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import classNames from 'classnames'
22
import { useCallback, useMemo, useState } from 'react'
33
import type { ClimateSettingSchedule } from 'seamapi'
44

5+
import { useComponentTelemetry } from 'lib/telemetry/index.js'
6+
57
import { compareByCreatedAtDesc } from 'lib/dates.js'
68
import { NestedClimateSettingScheduleDetails } from 'lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDetails.js'
79
import { ClimateSettingScheduleRow } from 'lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleRow.js'
@@ -66,6 +68,8 @@ export function ClimateSettingScheduleTable({
6668
disableCreateAccessCode,
6769
disableEditAccessCode,
6870
}: ClimateSettingScheduleTableProps): JSX.Element {
71+
useComponentTelemetry('ClimateSettingScheduleTable')
72+
6973
const { climateSettingSchedules, isLoading, isError, error } =
7074
useClimateSettingSchedules({
7175
device_id: deviceId,

src/lib/seam/components/ConnectAccountButton/ConnectAccountButton.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { useCallback } from 'react'
22

3+
import { useComponentTelemetry } from 'lib/telemetry/index.js'
4+
35
import {
46
type CommonProps,
57
withRequiredCommonProps,
@@ -15,6 +17,8 @@ export const NestedConnectAccountButton =
1517
export function ConnectAccountButton({
1618
className,
1719
}: ConnectAccountButtonProps = {}): JSX.Element {
20+
useComponentTelemetry('ConnectAccountButton')
21+
1822
const { isLoading, mutate } = useCreateConnectWebview({
1923
willNavigateToWebview: true,
2024
})

src/lib/seam/components/CreateAccessCodeForm/CreateAccessCodeForm.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { useState } from 'react'
22
import type { SeamError } from 'seamapi'
33

4+
import { useComponentTelemetry } from 'lib/telemetry/index.js'
5+
46
import { createIsoDate } from 'lib/dates.js'
57
import { useCreateAccessCode } from 'lib/seam/access-codes/use-create-access-code.js'
68
import {
@@ -26,6 +28,8 @@ export function CreateAccessCodeForm({
2628
onBack,
2729
deviceId,
2830
}: CreateAccessCodeFormProps): JSX.Element | null {
31+
useComponentTelemetry('CreateAccessCodeForm')
32+
2933
const { device } = useDevice({
3034
device_id: deviceId,
3135
})

src/lib/seam/components/CreateClimateSettingScheduleForm/CreateClimateSettingScheduleForm.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { useComponentTelemetry } from 'lib/telemetry/index.js'
2+
13
import { createIsoDate } from 'lib/dates.js'
24
import type { CommonProps } from 'lib/seam/components/common-props.js'
35
import { useCreateClimateSettingSchedule } from 'lib/seam/thermostats/climate-setting-schedules/use-create-climate-setting-schedule.js'
@@ -12,6 +14,8 @@ export function CreateClimateSettingScheduleForm({
1214
className,
1315
onBack,
1416
}: CreateClimateSettingScheduleFormProps): JSX.Element | null {
17+
useComponentTelemetry('CreateClimateSettingScheduleForm')
18+
1519
const { submit, isSubmitting } = useSubmitCreateClimateSettingSchedule(onBack)
1620

1721
return (

src/lib/seam/components/DeviceDetails/DeviceDetails.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { isLockDevice, isThermostatDevice } from 'seamapi'
22

3+
import { useComponentTelemetry } from 'lib/telemetry/index.js'
4+
35
import {
46
type CommonProps,
57
withRequiredCommonProps,
@@ -21,6 +23,8 @@ export function DeviceDetails({
2123
onBack,
2224
className,
2325
}: DeviceDetailsProps): JSX.Element | null {
26+
useComponentTelemetry('DeviceDetails')
27+
2428
const { device } = useDevice({
2529
device_id: deviceId,
2630
})

src/lib/seam/components/DeviceTable/DeviceTable.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import classNames from 'classnames'
22
import { useCallback, useMemo, useState } from 'react'
33
import { type CommonDevice, isLockDevice, isThermostatDevice } from 'seamapi'
44

5+
import { useComponentTelemetry } from 'lib/telemetry/index.js'
6+
57
import { compareByCreatedAtDesc } from 'lib/dates.js'
68
import {
79
type CommonProps,
@@ -71,6 +73,8 @@ export function DeviceTable({
7173
onBack,
7274
className,
7375
}: DeviceTableProps = {}): JSX.Element {
76+
useComponentTelemetry('DeviceTable')
77+
7478
const { devices, isLoading, isError, error } = useDevices({
7579
device_ids: deviceIds,
7680
connected_account_ids: connectedAccountIds,

src/lib/seam/components/EditAccessCodeForm/EditAccessCodeForm.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { useState } from 'react'
22
import type { SeamError } from 'seamapi'
33

4+
import { useComponentTelemetry } from 'lib/telemetry/index.js'
5+
46
import { createIsoDate } from 'lib/dates.js'
57
import {
68
useAccessCode,
@@ -30,6 +32,8 @@ export function EditAccessCodeForm({
3032
onBack,
3133
className,
3234
}: EditAccessCodeFormProps): JSX.Element | null {
35+
useComponentTelemetry('EditAccessCodeForm')
36+
3337
const { accessCode } = useAccessCode({
3438
access_code_id: accessCodeId,
3539
})

0 commit comments

Comments
 (0)