diff --git a/.changeset/tiny-beans-appear.md b/.changeset/tiny-beans-appear.md new file mode 100644 index 000000000..31b99d1f4 --- /dev/null +++ b/.changeset/tiny-beans-appear.md @@ -0,0 +1,5 @@ +--- +"@scaleway/use-analytics": patch +--- + +Create Analytics providers with rudderstack, add consent management diff --git a/package.json b/package.json index d4eb79156..2e3e034d3 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@commitlint/cli": "catalog:", "@commitlint/config-conventional": "catalog:", "@eslint/eslintrc": "catalog:", + "@rudderstack/analytics-js": "catalog:", "@scaleway/eslint-config-react": "workspace:*", "@scaleway/tsconfig": "workspace:*", "@testing-library/jest-dom": "catalog:", @@ -72,7 +73,12 @@ "react-dom": "18 || 19", "@types/react": "18 || 19" } - } + }, + "onlyBuiltDependencies": [ + "@biomejs/biome", + "esbuild", + "unrs-resolver" + ] }, "commitlint": { "extends": [ diff --git a/packages/use-analytics/.eslintignore b/packages/use-analytics/.eslintignore new file mode 100644 index 000000000..68f8bbb0d --- /dev/null +++ b/packages/use-analytics/.eslintignore @@ -0,0 +1,5 @@ +dist/ +coverage/ +node_modules +.reports/ +coverage/ \ No newline at end of file diff --git a/packages/use-analytics/.eslintrc.cjs b/packages/use-analytics/.eslintrc.cjs new file mode 100644 index 000000000..a2dbbe3d7 --- /dev/null +++ b/packages/use-analytics/.eslintrc.cjs @@ -0,0 +1,10 @@ +const { join } = require('path') + +module.exports = { + rules: { + 'import/no-extraneous-dependencies': [ + 'error', + { packageDir: [__dirname, join(__dirname, '../../')] }, + ], + }, +} diff --git a/packages/use-analytics/.npmignore b/packages/use-analytics/.npmignore new file mode 100644 index 000000000..5600eef5f --- /dev/null +++ b/packages/use-analytics/.npmignore @@ -0,0 +1,5 @@ +**/__tests__/** +examples/ +src +.eslintrc.cjs +!.npmignore diff --git a/packages/use-analytics/CHANGELOG.md b/packages/use-analytics/CHANGELOG.md new file mode 100644 index 000000000..420e6f23d --- /dev/null +++ b/packages/use-analytics/CHANGELOG.md @@ -0,0 +1 @@ +# Change Log diff --git a/packages/use-analytics/README.md b/packages/use-analytics/README.md new file mode 100644 index 000000000..7d1caadd8 --- /dev/null +++ b/packages/use-analytics/README.md @@ -0,0 +1,96 @@ +# `@scaleway/use-analytics` + +## A tiny hooks to handle analytics events + +## Install + +```bash +$ pnpm add @scaleway/use-analytics +``` + +## Usage + +### Event directory + +Create an events directory with all you specific events. + +``` +events + ┣ pageTypes + ┃ ┗ index.ts + ┣ 📂loginEvent + ┃ ┗ index.ts + ┃ index.ts ( export all you functions ) + +``` + +Each event will have a format like this: + +```typescript +const pageVisited = + (analytics?: Analytics) => + async (args: Args): Promise => { + // here do what you have to do with analytics + await analytics?.page(args) + } + +export default pageVisited +``` + +```typescript +import pageTypes from './pageTypes' +import testEvents from './testEvents' + +export default { + pageTypes, + testEvents, +} +``` + +### Context Load + +Inside you global app you have to use our Analytics Provider to allow loading of analytics from your settting app. +This will trigger a load and return analitycs function inside you provider. + +```javascript +import { AnalyticsProvider } from '@scaleway/use-analytics' +import { captureMessage } from '@sentry/browser' +import events from './events' + +const App = () => ( + captureMessage(`Error on Analytics: ${e.message}`)} + > + + +) +``` + +### Hook utility + +Now you maybe want to use your events inside your app . +If you are using typescript, you may + +```typescript +import { useAnalytics } from '@scaleway/use-analytics' +import type { Events } from 'types/events' +import { Form, Submit } from '@scaleway/form' + +const Login = () => { + const { events } = useAnalytics() + + const onSubmit = async args => { + // make you api calls + await events.login() + } + + return ( +
+ // others fields + + + ) +} +``` diff --git a/packages/use-analytics/package.json b/packages/use-analytics/package.json new file mode 100644 index 000000000..698349908 --- /dev/null +++ b/packages/use-analytics/package.json @@ -0,0 +1,64 @@ +{ + "name": "@scaleway/use-analytics", + "version": "0.0.0", + "description": "A small hook to handle events analytics", + "engines": { + "node": ">=20.x" + }, + "main": "./dist/index.cjs", + "sideEffects": false, + "type": "module", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/analytics/index.d.ts", + "require": "./dist/analytics/index.cjs", + "default": "./dist/analytics/index.js" + }, + "./cookies-consent": { + "types": "./dist/cookies-consent/index.d.ts", + "require": "./dist/cookies-consent/index.cjs", + "default": "./dist/cookies-consent/index.js" + } + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "dist/*" + ], + "scripts": { + "prebuild": "shx rm -rf dist", + "typecheck": "tsc --noEmit", + "type:generate": "tsc --declaration -p tsconfig.build.json", + "build": "vite build --config vite.config.ts && pnpm run type:generate", + "build:profile": "npx vite-bundle-visualizer -c vite.config.ts", + "lint": "eslint --report-unused-disable-directives --cache --cache-strategy content --ext ts,tsx ." + }, + "repository": { + "type": "git", + "url": "https://github.com/scaleway/scaleway-lib", + "directory": "packages/use-analytics" + }, + "license": "MIT", + "keywords": [ + "react", + "reactjs", + "hooks", + "segment", + "rudderstack" + ], + "dependencies": { + "@rudderstack/analytics-js":"catalog:", + "@segment/analytics-next": "catalog:", + "cookie": "catalog:", + "use-deep-compare-effect": "catalog:" + }, + "devDependencies": { + "react": "catalog:" + }, + "peerDependencies": { + "react": "18.x || 19.x" + } +} diff --git a/packages/use-analytics/src/__tests__/AnalyticsProvider.test.tsx b/packages/use-analytics/src/__tests__/AnalyticsProvider.test.tsx new file mode 100644 index 000000000..1929a0986 --- /dev/null +++ b/packages/use-analytics/src/__tests__/AnalyticsProvider.test.tsx @@ -0,0 +1,160 @@ +// import { AnalyticsBrowser } from '@segment/analytics-next' +// import type { Context } from '@segment/analytics-next' +// import { RudderAnalytics } from '@rudderstack/analytics-js' +// import { render, screen, waitFor } from '@testing-library/react' +// import { describe, expect, it, vi } from 'vitest' +// import {AnalyticsProvider} from './' +// import type { Analytics } from './index' + +// const TestChildren = () =>
children
+ +// const defaultAnalytics = {} as Analytics + +// describe('AnalyticsProvider', () => { +// it('Provider should render children when shouldRenderOnlyWhenReady is false', async () => { +// const mock = vi +// .spyOn(RudderAnalytics, 'load') +// .mockResolvedValue([defaultAnalytics, {} as Context]) + +// const settings = { writeKey: 'helloworld', cdnURL: '', timeout: 300 } + +// render( +// () => Promise.resolve(), +// }} +// > +// +// , +// ) + +// await waitFor(() => { +// expect(mock).toHaveBeenCalledTimes(0) +// }) + +// expect(screen.getByTestId('test')).toBeTruthy() +// }) + +// it('Provider should not render children when options are not loaded ', async () => { +// const mock = vi +// .spyOn(AnalyticsBrowser, 'load') +// .mockResolvedValue([{} as Analytics, {} as Context]) + +// const settings = { writeKey: 'helloworld' } + +// render( +// () => Promise.resolve(), +// }} +// > +// +// , +// ) + +// await waitFor(() => { +// expect(mock).toHaveBeenCalledTimes(0) +// }) + +// expect(screen.queryByTestId('test')).toBe(null) +// }) + +// it('Provider should not render children when options are not loaded at first render, but load after options changed', async () => { +// const mock = vi +// .spyOn(AnalyticsBrowser, 'load') +// .mockResolvedValue([{} as Analytics, {} as Context]) + +// const settings = { writeKey: 'helloworld' } + +// const { rerender } = render( +// () => Promise.resolve(), +// }} +// > +// +// , +// ) + +// await waitFor(() => { +// expect(mock).toHaveBeenCalledTimes(0) +// }) + +// expect(screen.queryByTestId('test')).toBe(null) + +// rerender( +// () => Promise.resolve(), +// }} +// > +// +// , +// ) + +// await waitFor(() => { +// expect(mock).toHaveBeenCalledTimes(1) +// }) + +// expect(screen.queryByTestId('test')).toBeTruthy() +// }) + +// it('Provider should not render children when options are not loaded at first render, but load after options changed even without settings', async () => { +// const mock = vi +// .spyOn(AnalyticsBrowser, 'load') +// .mockResolvedValue([{} as Analytics, {} as Context]) + +// const { rerender } = render( +// () => Promise.resolve(), +// }} +// > +// +// , +// ) + +// await waitFor(() => { +// expect(mock).toHaveBeenCalledTimes(0) +// }) + +// expect(screen.queryByTestId('test')).toBe(null) + +// rerender( +// () => Promise.resolve(), +// }} +// > +// +// , +// ) + +// await waitFor(() => { +// expect(mock).toHaveBeenCalledTimes(0) +// }) + +// expect(screen.queryByTestId('test')).toBeTruthy() +// }) +// }) diff --git a/packages/use-analytics/src/__tests__/index.tsx b/packages/use-analytics/src/__tests__/index.tsx new file mode 100644 index 000000000..748d579bc --- /dev/null +++ b/packages/use-analytics/src/__tests__/index.tsx @@ -0,0 +1,397 @@ +// // import { AnalyticsBrowser } from '@segment/analytics-next' +// // import type { Context } from '@segment/analytics-next' +// import { renderHook, waitFor } from '@testing-library/react' +// import type { ReactNode } from 'react' +// import { beforeEach, describe, expect, it, vi } from 'vitest' +// import waitForExpect from 'wait-for-expect' +// import { useAnalytics, AnalyticsProvider } from '..' +// import type { Analytics, OnEventError, AnalyticsProviderProps } from '..' + +// const eventError = new Error('Error Event') + +// const defaultEvents = { +// errorEvent: (_?: Analytics, onEventError?: OnEventError) => async () => { +// try { +// await new Promise((__, reject) => { +// reject(eventError) +// }) +// } catch (error) { +// await onEventError?.(error as Error) +// } +// }, +// pageVisited: +// (analytics?: Analytics) => +// async ( +// pageType: 'Docs' | 'Blog' | 'Main', +// organizationId: string, +// productCategory?: 'Dedibox' | 'Elements' | 'Datacenter', +// ): Promise => { +// analytics?.page( +// { +// page_type: pageType, +// product_category: productCategory, +// }, +// { +// context: { +// groupId: organizationId, +// }, +// }, +// ) +// }, +// } + +// type DefaultEvents = typeof defaultEvents + +// const wrapper = +// ({ +// settings, +// initOptions, +// areOptionsLoaded, +// onError, +// onEventError, +// events = defaultEvents, +// shouldRenderOnlyWhenReady, +// }: Omit, 'children'>) => +// ({ children }: { children: ReactNode }) => ( +// +// {children} +// +// ) + +// describe('segment hook', () => { +// beforeEach(() => { +// vi.clearAllMocks() +// }) + +// afterAll(() => { +// vi.restoreAllMocks() +// }) + +// it('useAnalytics should not be defined without AnalyticsProvider', () => { +// const orignalConsoleError = console.error +// console.error = vi.fn + +// try { +// renderHook(() => useAnalytics()) +// } catch (error) { +// expect((error as Error).message).toBe( +// 'useAnalytics must be used within a AnalyticsProvider', +// ) +// } + +// console.error = orignalConsoleError +// }) + +// it('useAnalytics should not be ready and not load by default', () => { +// const mock = vi +// .spyOn(AnalyticsBrowser, 'load') +// .mockResolvedValue([{} as Analytics, {} as Context]) + +// const { result } = renderHook(() => useAnalytics(), { +// wrapper: wrapper({ +// events: defaultEvents, +// settings: undefined, +// }), +// }) + +// expect(mock).toHaveBeenCalledTimes(0) +// expect(result.current.analytics).toBe(undefined) +// expect(result.current.isAnalyticsReady).toBe(false) +// }) + +// it('useAnalytics should not load without settings', () => { +// const { result } = renderHook(() => useAnalytics(), { +// wrapper: wrapper({ +// events: defaultEvents, +// settings: undefined, +// }), +// }) +// expect(result.current.analytics).toBe(undefined) +// expect(result.current.isAnalyticsReady).toBe(false) +// }) + +// it('useAnalytics should not load without initOptions', () => { +// const { result } = renderHook(() => useAnalytics(), { +// wrapper: wrapper({ +// events: defaultEvents, +// settings: { writeKey: 'sample ' }, +// initOptions: undefined, +// }), +// }) +// expect(result.current.analytics).toBe(undefined) +// expect(result.current.isAnalyticsReady).toBe(false) +// }) + +// it('useAnalytics should not load but be ready when All integrations disabled', async () => { +// const mock = vi +// .spyOn(AnalyticsBrowser, 'load') +// .mockResolvedValue([{} as Analytics, {} as Context]) + +// const { result } = renderHook(() => useAnalytics(), { +// wrapper: wrapper({ +// events: defaultEvents, +// initOptions: { integrations: { All: false } }, +// settings: { writeKey: 'sample ' }, +// areOptionsLoaded: true, +// }), +// }) +// await waitFor(() => { +// expect(mock).toHaveBeenCalledTimes(0) +// }) + +// expect(result.current.analytics).toBe(undefined) +// expect(result.current.isAnalyticsReady).toBe(true) +// }) + +// it('useAnalytics should not load but be ready when all integrations are disabled ', async () => { +// const mock = vi +// .spyOn(AnalyticsBrowser, 'load') +// .mockResolvedValue([{} as Analytics, {} as Context]) + +// const { result } = renderHook(() => useAnalytics(), { +// wrapper: wrapper({ +// events: defaultEvents, +// initOptions: { +// integrations: { +// testInteg: false, +// testInteg2: false, +// testInteg3: false, +// }, +// }, +// settings: { writeKey: 'sample ' }, +// areOptionsLoaded: true, +// }), +// }) +// await waitFor(() => { +// expect(mock).toHaveBeenCalledTimes(0) +// }) + +// await waitFor(() => { +// expect(result.current.analytics).toStrictEqual(undefined) +// }) +// expect(result.current.isAnalyticsReady).toBe(true) +// }) + +// it('useAnalytics should load when at least one integrations enabled', async () => { +// const mock = vi +// .spyOn(AnalyticsBrowser, 'load') +// .mockResolvedValue([{} as Analytics, {} as Context]) + +// const { result } = renderHook(() => useAnalytics(), { +// wrapper: wrapper({ +// events: defaultEvents, +// initOptions: { +// integrations: { +// testInteg: false, +// testInteg2: true, +// testInteg3: false, +// }, +// }, +// settings: { writeKey: 'sample ' }, +// areOptionsLoaded: true, +// }), +// }) +// await waitFor(() => { +// expect(mock).toHaveBeenCalledTimes(1) +// }) + +// await waitFor(() => { +// expect(result.current.analytics).toStrictEqual({}) +// }) +// expect(result.current.isAnalyticsReady).toBe(true) +// }) + +// it('Provider should not load when options are not loaded', async () => { +// const mock = vi +// .spyOn(AnalyticsBrowser, 'load') +// .mockResolvedValue([{} as Analytics, {} as Context]) + +// const settings = { writeKey: 'helloworld' } + +// const { result } = renderHook(() => useAnalytics(), { +// wrapper: wrapper({ +// events: defaultEvents, +// settings, +// initOptions: {}, +// areOptionsLoaded: false, +// }), +// }) +// await waitFor(() => { +// expect(mock).toHaveBeenCalledTimes(0) +// }) + +// await waitFor(() => { +// expect(result.current.analytics).toStrictEqual(undefined) +// }) +// expect(result.current.isAnalyticsReady).toBe(false) +// }) + +// it('Provider should load with key', async () => { +// const mock = vi +// .spyOn(AnalyticsBrowser, 'load') +// .mockResolvedValue([{} as Analytics, {} as Context]) + +// const settings = { writeKey: 'helloworld' } + +// const { result } = renderHook(() => useAnalytics(), { +// wrapper: wrapper({ +// events: defaultEvents, +// settings, +// areOptionsLoaded: true, +// }), +// }) + +// await waitFor(() => { +// expect(mock).toHaveBeenCalledTimes(1) +// expect(mock).toHaveBeenCalledWith(settings, undefined) +// }) + +// await waitFor(() => { +// expect(result.current.analytics).toStrictEqual({}) +// }) +// expect(result.current.isAnalyticsReady).toBe(true) +// }) + +// it('Provider should load with key and cdn', async () => { +// const mock = vi +// .spyOn(AnalyticsBrowser, 'load') +// .mockResolvedValue([{} as Analytics, {} as Context]) + +// const settings = { cdn: 'https://cdn.proxy', writeKey: 'helloworld' } + +// const { result } = renderHook(() => useAnalytics(), { +// wrapper: wrapper({ +// events: defaultEvents, +// settings, +// areOptionsLoaded: true, +// }), +// }) +// await waitFor(() => { +// expect(mock).toHaveBeenCalledTimes(1) +// expect(mock).toHaveBeenCalledWith(settings, undefined) +// }) + +// await waitFor(() => { +// expect(result.current.analytics).toStrictEqual({}) +// }) +// expect(result.current.isAnalyticsReady).toBe(true) +// }) + +// it('Provider should load and call onError on analytics load error', async () => { +// const error = new Error('not good') +// const mock = vi.spyOn(AnalyticsBrowser, 'load').mockRejectedValue(error) + +// const onError = vi.fn() +// const settings = { writeKey: 'pleasethrow' } + +// const { result } = renderHook(() => useAnalytics(), { +// wrapper: wrapper({ +// events: defaultEvents, +// onError, +// settings, +// areOptionsLoaded: true, +// }), +// }) +// await waitFor(() => { +// expect(mock).toHaveBeenCalledTimes(1) +// expect(mock).toHaveBeenCalledWith(settings, undefined) +// }) + +// await waitForExpect(() => { +// expect(onError).toHaveBeenCalledTimes(1) +// }) +// expect(onError).toHaveBeenCalledWith(error) +// await waitForExpect(() => { +// expect(result.current.isAnalyticsReady).toBe(true) +// }) +// }) + +// it('Provider call onEventError when an event is trigger with an error', async () => { +// const mock = vi +// .spyOn(AnalyticsBrowser, 'load') +// .mockResolvedValue([{} as Analytics, {} as Context]) + +// const onEventError = vi.fn() +// const onError = vi.fn() + +// const settings = { writeKey: 'pleasethrow' } + +// const { result } = renderHook(() => useAnalytics(), { +// wrapper: wrapper({ +// events: defaultEvents, +// onError, +// onEventError, +// settings, +// areOptionsLoaded: true, +// }), +// }) +// await waitFor(() => { +// expect(mock).toHaveBeenCalledTimes(1) +// }) + +// await waitFor(async () => { +// await result.current.events.errorEvent() +// }) + +// await waitForExpect(() => { +// expect(onEventError).toHaveBeenCalledTimes(1) +// expect(onEventError).toHaveBeenCalledWith(eventError) +// }) +// expect(result.current.isAnalyticsReady).toBe(true) +// }) + +// it('Provider should load with settings and initOptions', async () => { +// const mock = vi +// .spyOn(AnalyticsBrowser, 'load') +// .mockResolvedValue([{} as Analytics, {} as Context]) + +// const settings = { writeKey: 'helloworld' } +// const initOptions = { +// initialPageview: false, +// } + +// const { result } = renderHook(() => useAnalytics(), { +// wrapper: wrapper({ +// events: defaultEvents, +// initOptions, +// settings, +// areOptionsLoaded: true, +// }), +// }) + +// await waitFor(() => { +// expect(mock).toHaveBeenCalledTimes(1) +// expect(mock).toHaveBeenCalledWith(settings, initOptions) +// }) + +// await waitFor(() => { +// expect(result.current.analytics).toStrictEqual({}) +// }) +// expect(result.current.isAnalyticsReady).toBe(true) +// }) + +// it('useAnalytics should correctly infer types', async () => { +// const { result } = renderHook(() => useAnalytics(), { +// wrapper: wrapper({ +// events: defaultEvents, +// settings: undefined, +// }), +// }) + +// expect( +// await result.current.events.pageVisited( +// 'Main', +// 'organizationId', +// 'Elements', +// ), +// ).toBe(undefined) +// }) +// }) diff --git a/packages/use-analytics/src/analytics/constants.ts b/packages/use-analytics/src/analytics/constants.ts new file mode 100644 index 000000000..0a212a58c --- /dev/null +++ b/packages/use-analytics/src/analytics/constants.ts @@ -0,0 +1,61 @@ +import type { ConsentOptions, LoadOptions } from '@rudderstack/analytics-js' + +export const consentOptions: ConsentOptions = { + trackConsent: true, + discardPreConsentEvents: true, + storage: { + type: 'localStorage', + }, + consentManagement: { + enabled: true, + allowedConsentIds: [], + deniedConsentIds: [], + }, +} + +export const defaultLoadOptions: LoadOptions = { + logLevel: 'NONE', + polyfillIfRequired: false, + preConsent: { + enabled: true, + storage: { + strategy: 'anonymousId', + }, + events: { + delivery: 'buffer', + }, + }, + consentManagement: { + enabled: true, + provider: 'custom', + // https://www.rudderstack.com/docs/data-governance/consent-management/custom-consent-manager/javascript/#pre-consent-user-tracking + allowedConsentIds: [], + deniedConsentIds: [], + }, + queueOptions: { + batch: { + enabled: true, + maxItems: 20, + maxSize: 512 * 1024, // 512 KB + flushInterval: 3_000, // in ms + }, + }, + /** + * integrations are usefull in case you do not want to load uses some destinations despites the consentManagements or if you need to change something. + * By default it's will be set to All and we let the consent Managements system handle the load of client destinations. + */ + integrations: { + All: true, + }, + loadIntegration: false, + secureCookie: true, + anonymousIdOptions: { + autoCapture: { + enabled: true, + }, + }, + sessions: { + autoTrack: true, + timeout: 500, + }, +} diff --git a/packages/use-analytics/src/analytics/index.ts b/packages/use-analytics/src/analytics/index.ts new file mode 100644 index 000000000..0f2f85ff3 --- /dev/null +++ b/packages/use-analytics/src/analytics/index.ts @@ -0,0 +1,11 @@ +export { AnalyticsProvider, useAnalytics } from './useAnalytics' + +export type { + Analytics, + OnEventError, + AnalyticsProviderProps, +} from './useAnalytics' + +export { userMigrationsTraits } from './segments/userMigrationsTraits' +export { defaultLoadOptions } from './constants' +export { useDestinations } from './useDestinations' diff --git a/packages/use-analytics/src/analytics/segments/trackLink.ts b/packages/use-analytics/src/analytics/segments/trackLink.ts new file mode 100644 index 000000000..dc933ad20 --- /dev/null +++ b/packages/use-analytics/src/analytics/segments/trackLink.ts @@ -0,0 +1,19 @@ +import type { RudderAnalytics } from '@rudderstack/analytics-js' +import type { + EventProperties, + Analytics as SegmentAnalytics, +} from '@segment/analytics-next' + +export type TrackLink = SegmentAnalytics['trackLink'] + +/** + * @deprecated + * this function is a wrapper of a Track to facilitate the migration from segment to rudderstack + */ +export const trackLink = + (analytics: RudderAnalytics) => + (...args: Parameters) => { + const [, event, properties] = args + + return analytics.track(event as string, properties as EventProperties) + } diff --git a/packages/use-analytics/src/analytics/segments/userMigrationsTraits.ts b/packages/use-analytics/src/analytics/segments/userMigrationsTraits.ts new file mode 100644 index 000000000..c833ab16c --- /dev/null +++ b/packages/use-analytics/src/analytics/segments/userMigrationsTraits.ts @@ -0,0 +1,29 @@ +import type { RudderAnalytics } from '@rudderstack/analytics-js' + +const SEGMENT_COOKIES_KEY = { + ANONYMOUS_ID: 'ajs_anonymous_id', + USER_ID: 'ajs_user_id', + GROUP_ID: 'ajs_group_id', +} + +export const userMigrationsTraits = (rudderAnalytics: RudderAnalytics) => { + const segmentAnonymousId = localStorage.getItem( + SEGMENT_COOKIES_KEY.ANONYMOUS_ID, + ) + const segmentUserId = localStorage.getItem(SEGMENT_COOKIES_KEY.USER_ID) + const segmentGroupId = localStorage.getItem(SEGMENT_COOKIES_KEY.GROUP_ID) + const rudderUserId = rudderAnalytics.getUserId() + const rudderGroupId = rudderAnalytics.getGroupId() + + if (segmentAnonymousId) { + rudderAnalytics.setAnonymousId(segmentAnonymousId) + } + + if (segmentUserId && (!rudderUserId || rudderUserId !== segmentUserId)) { + rudderAnalytics.identify(segmentUserId) + } + + if (segmentGroupId && (!rudderGroupId || rudderGroupId !== segmentGroupId)) { + rudderAnalytics.group(segmentGroupId) + } +} diff --git a/packages/use-analytics/src/analytics/useAnalytics.tsx b/packages/use-analytics/src/analytics/useAnalytics.tsx new file mode 100644 index 000000000..452000d08 --- /dev/null +++ b/packages/use-analytics/src/analytics/useAnalytics.tsx @@ -0,0 +1,174 @@ +import { RudderAnalytics } from '@rudderstack/analytics-js' +import type { LoadOptions } from '@rudderstack/analytics-js' +import { createContext, useContext, useEffect, useMemo, useState } from 'react' +import type { ReactNode } from 'react' +import { useDeepCompareEffectNoCheck } from 'use-deep-compare-effect' +import { destSDKBaseURL, pluginsSDKBaseURL } from '../constants' +import type { CategoryKind } from '../types' +import { consentOptions, defaultLoadOptions } from './constants' +import { trackLink } from './segments/trackLink' +import type { TrackLink } from './segments/trackLink' +import { userMigrationsTraits } from './segments/userMigrationsTraits' + +type Analytics = RudderAnalytics & { + trackLink: TrackLink +} + +export type { Analytics } + +export type OnEventError = (error: Error) => Promise | void + +type EventFunction = (...args: never[]) => Promise +type Events = Record< + string, + (analytics?: Analytics, onEventError?: OnEventError) => EventFunction +> + +type AnalyticsContextInterface = { + analytics: Analytics | undefined + events: { [K in keyof T]: ReturnType } + isAnalyticsReady: boolean +} + +const AnalyticsContext = createContext( + undefined, +) + +export function useAnalytics(): AnalyticsContextInterface { + const context = useContext | undefined>( + // @ts-expect-error Here we force cast the generic onto the useContext because the context is a + // global variable and cannot be generic + AnalyticsContext, + ) + if (context === undefined) { + throw new Error('useAnalytics must be used within a AnalyticsProvider') + } + + return context +} + +export type AnalyticsProviderProps = { + settings?: { + writeKey: string + cdnURL: string + } + loadOptions?: LoadOptions + + /** + * This option help you in case you don't want to load analytics + */ + shouldLoadAnalytics?: boolean + /** + * // This option force provider to render children only when isAnalytics is ready + */ + shouldRenderOnlyWhenReady?: boolean + allowedConsents: CategoryKind[] + deniedConsents: CategoryKind[] + onError?: (err: Error) => void + onEventError?: OnEventError + events: T + children: ReactNode + /** + * This can be used to set consentManagement or modify the config + */ + onLoaded: (analytics: Analytics) => void +} + +export function AnalyticsProvider({ + children, + settings, + loadOptions, + shouldRenderOnlyWhenReady = false, + shouldLoadAnalytics = false, + onError, + onEventError, + allowedConsents, + deniedConsents, + events, +}: AnalyticsProviderProps) { + const [isAnalyticsReady, setIsAnalyticsReady] = useState(false) + const [internalAnalytics, setAnalytics] = useState( + undefined, + ) + + const shouldLoad = useMemo(() => { + if (shouldLoadAnalytics) { + return !!settings?.writeKey + } + + return false + }, [shouldLoadAnalytics, settings?.writeKey]) + + useDeepCompareEffectNoCheck(() => { + if (shouldLoad && settings) { + const analytics = new RudderAnalytics() + + analytics.load(settings.writeKey, settings.cdnURL, { + ...defaultLoadOptions, + destSDKBaseURL: destSDKBaseURL(settings.cdnURL), + pluginsSDKBaseURL: pluginsSDKBaseURL(settings.cdnURL), + onLoaded: (rudderAnalytics: Analytics) => { + userMigrationsTraits(rudderAnalytics) + + rudderAnalytics.consent({ + ...consentOptions, + consentManagement: { + enabled: true, + allowedConsentIds: allowedConsents, + deniedConsentIds: deniedConsents, + }, + }) + + setIsAnalyticsReady(true) + }, + ...loadOptions, + }) + + analytics.ready(() => { + // @ts-expect-error blabla + setAnalytics({ ...analytics, trackLink: trackLink(analytics) }) + setIsAnalyticsReady(true) + }) + } else if (shouldLoadAnalytics && !shouldLoad) { + // When user has refused tracking, set ready anyway + setIsAnalyticsReady(true) + } + }, [onError, settings, loadOptions, shouldLoad, shouldLoadAnalytics]) + + useEffect(() => { + if (isAnalyticsReady) { + internalAnalytics?.consent({ + consentManagement: { + allowedConsentIds: allowedConsents, + deniedConsentIds: deniedConsents, + }, + }) + } + }, [internalAnalytics, isAnalyticsReady, allowedConsents, deniedConsents]) + + const value = useMemo>(() => { + const curiedEvents = Object.entries(events).reduce( + (acc, [eventName, eventFn]) => ({ + ...acc, + [eventName]: eventFn(internalAnalytics, onEventError), + }), + {}, + ) as { [K in keyof T]: ReturnType } + + return { + analytics: internalAnalytics, + events: curiedEvents, + isAnalyticsReady, + } + }, [events, internalAnalytics, isAnalyticsReady, onEventError]) + + const shouldRender = !shouldRenderOnlyWhenReady || isAnalyticsReady + + return ( + + {shouldRender ? children : null} + + ) +} + +export default AnalyticsProvider diff --git a/packages/use-analytics/src/analytics/useDestinations.ts b/packages/use-analytics/src/analytics/useDestinations.ts new file mode 100644 index 000000000..e32261b8a --- /dev/null +++ b/packages/use-analytics/src/analytics/useDestinations.ts @@ -0,0 +1,79 @@ +import { useEffect, useState } from 'react' +import type { AnalyticsConfig, AnalyticsIntegration, Config } from '../types' + +const timeout = (time: number) => { + const controller = new AbortController() + setTimeout(() => controller.abort(), time * 1000) + + return controller +} + +const transformConfigToDestinations = ( + config: AnalyticsConfig, +): AnalyticsIntegration[] => { + const { destinations } = config.source + + const integrations = destinations.map( + ({ destinationDefinition, config: { consentManagement } }) => ({ + name: destinationDefinition.name, + displayName: destinationDefinition.displayName, + consents: consentManagement.flatMap(({ consents }) => + consents.map(({ consent }) => consent), + ), + }), + ) + + return integrations +} + +/** + * Return only Client/Hybrid destinations, Cloud Mode destinations will not be return. + * Should be the most important as only theses destinations will load a script and set an external cookies. + * Will return undefined if loading, empty array if no response or error, response else. + */ +export const useDestinations = (config: Config) => { + const [destinations, setDestinations] = useState< + AnalyticsIntegration[] | undefined + >(undefined) + + useEffect(() => { + const fetchDestinations = async () => { + if (config.analytics?.cdnURL && config.analytics.writeKey) { + const url = `${config.analytics.cdnURL}/sourceConfig` + const WRITE_KEY = window.btoa(`${config.analytics.writeKey}:`) + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: `Basic ${WRITE_KEY}`, + }, + // We'd rather have an half consent than no consent at all + signal: timeout(10).signal, + }) + if (!response.ok) { + throw new Error('Failed to fetch integrations from source') + } + const json = (await response.json()) as AnalyticsConfig + + return transformConfigToDestinations(json) + } + + return [] + } + + fetchDestinations() + .then(response => { + setDestinations(response) + }) + .catch(() => { + setDestinations([]) + }) + .finally(() => { + setDestinations([]) + }) + }, [setDestinations, config.analytics]) + + return { + destinations, + isLoaded: destinations !== undefined, + } +} diff --git a/packages/use-analytics/src/constants.ts b/packages/use-analytics/src/constants.ts new file mode 100644 index 000000000..66d90ee90 --- /dev/null +++ b/packages/use-analytics/src/constants.ts @@ -0,0 +1,28 @@ +import type { SerializeOptions } from 'cookie' + +export const CATEGORIES = [ + 'essential', + 'functional', + 'marketing', + 'analytics', + 'advertising', +] as const + +export const destSDKBaseURL = (cdnUrl: string) => + `${cdnUrl}/cdn/v3/modern/js-integrations` +export const pluginsSDKBaseURL = (cdnUrl: string) => + `${cdnUrl}/cdn/v3/modern/plugins` + +export const COOKIE_PREFIX = '_scw_rgpd' +export const HASH_COOKIE = `${COOKIE_PREFIX}_hash` + +// Appx 13 Months +export const CONSENT_MAX_AGE = 13 * 30 * 24 * 60 * 60 +// Appx 6 Months +export const CONSENT_ADVERTISING_MAX_AGE = 6 * 30 * 24 * 60 * 60 + +export const COOKIES_OPTIONS: SerializeOptions = { + sameSite: 'strict', + secure: true, + path: '/', +} as const diff --git a/packages/use-analytics/src/cookies-consent/CookieConsentProvider.tsx b/packages/use-analytics/src/cookies-consent/CookieConsentProvider.tsx new file mode 100644 index 000000000..adc78a97d --- /dev/null +++ b/packages/use-analytics/src/cookies-consent/CookieConsentProvider.tsx @@ -0,0 +1,219 @@ +import { parse, serialize } from 'cookie' +import type { SerializeOptions } from 'cookie' +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react' +import type { PropsWithChildren } from 'react' +import { useDestinations } from '../analytics/useDestinations' +import { + CATEGORIES, + CONSENT_ADVERTISING_MAX_AGE, + CONSENT_MAX_AGE, + COOKIES_OPTIONS, + COOKIE_PREFIX, + HASH_COOKIE, +} from '../constants' +import { uniq } from '../helpers/array' +import { IS_CLIENT } from '../helpers/isClient' +import { stringToHash } from '../helpers/misc' +import { isCategoryKind } from '../types' +import type { Config, Consent, Integration, Integrations } from '../types' + +type Context = { + destinations: Integrations + needConsent: boolean + isDestinationsLoaded: boolean + categories: typeof CATEGORIES + categoriesConsent: Partial + saveConsent: (categoriesConsent: Partial) => void +} + +const CookieConsentContext = createContext(undefined) + +export const useCookieConsent = (): Context => { + const context = useContext(CookieConsentContext) + if (context === undefined) { + throw new Error( + 'useCookieConsent must be used within a CookieConsentProvider', + ) + } + + return context +} + +export const CookieConsentProvider = ({ + children, + isConsentRequired, + essentialDestinations, + config, + cookiePrefix = COOKIE_PREFIX, + consentMaxAge = CONSENT_MAX_AGE, + consentAdvertisingMaxAge = CONSENT_ADVERTISING_MAX_AGE, + cookiesOptions = COOKIES_OPTIONS, +}: PropsWithChildren<{ + isConsentRequired: boolean + essentialDestinations: string[] + config: Config + cookiePrefix?: string + consentMaxAge?: number + consentAdvertisingMaxAge?: number + cookiesOptions?: SerializeOptions +}>) => { + const [needConsent, setNeedsConsent] = useState(false) + const [cookies, setCookies] = useState>( + IS_CLIENT ? parse(document.cookie) : {}, + ) + + const { + destinations: analyticsDestinations, + isLoaded: isDestinationsLoaded, + } = useDestinations(config) + + const destinations: Integrations = useMemo( + () => + uniq([ + ...(analyticsDestinations ?? []).map( + dest => + ({ + name: dest.name, + category: dest.consents[0] ?? 'essential', + }) satisfies Integration, + ), + ...essentialDestinations.map( + dest => + ({ + name: dest, + category: 'essential', + }) satisfies Integration, + ), + ]), + [analyticsDestinations, essentialDestinations], + ) + + // We compute a hash with all the integrations that are enabled + // This hash will be used to know if we need to ask for consent + // when a new integration is added + const destinationsHash = useMemo( + () => + stringToHash( + uniq([ + ...destinations.map(({ name }) => name), + ...essentialDestinations, + ]) + .sort() + .join(undefined), + ), + [destinations, essentialDestinations], + ) + + useEffect(() => { + // We set needConsent at false until we have an answer from segment + // This is to avoid showing setting needConsent to true only to be set + // to false after receiving segment answer and flicker the UI + + setNeedsConsent( + isConsentRequired && + cookies[HASH_COOKIE] !== destinationsHash.toString() && + analyticsDestinations !== undefined, + ) + }, [isConsentRequired, destinationsHash, analyticsDestinations, cookies]) + + // From the unique categories names we can now build our consent object + // and check if there is already a consent in a cookie + // Default consent if none is found is false + const cookieConsent = useMemo( + () => + CATEGORIES.reduce>( + (acc, category) => ({ + ...acc, + [category]: + isConsentRequired || needConsent + ? cookies[`${cookiePrefix}_${category}`] === 'true' + : true, + }), + {}, + ), + [isConsentRequired, cookiePrefix, needConsent, cookies], + ) + + const saveConsent = useCallback( + (categoriesConsent: Partial) => { + for (const [consentName, consentValue] of Object.entries( + categoriesConsent, + )) { + const consentCategoryName = isCategoryKind(consentName) + ? consentName + : 'unknown' + + const cookieName = `${cookiePrefix}_${consentCategoryName}` + + if (!consentValue) { + // If consent is set to false we have to delete the cookie + document.cookie = serialize(cookieName, '', { + ...cookiesOptions, + expires: new Date(0), + }) + } else { + document.cookie = serialize(cookieName, consentValue.toString(), { + ...cookiesOptions, + maxAge: + consentCategoryName === 'advertising' + ? consentAdvertisingMaxAge + : consentMaxAge, + }) + } + setCookies(prevCookies => ({ + ...prevCookies, + [cookieName]: consentValue ? 'true' : 'false', + })) + } + // We set the hash cookie to the current consented integrations + document.cookie = serialize(HASH_COOKIE, destinationsHash.toString(), { + ...cookiesOptions, + // Here we use the shortest max age to force to ask again for expired consent + maxAge: consentAdvertisingMaxAge, + }) + setCookies(prevCookies => ({ + ...prevCookies, + [HASH_COOKIE]: destinationsHash.toString(), + })) + setNeedsConsent(false) + }, + [ + destinationsHash, + consentAdvertisingMaxAge, + consentMaxAge, + cookiePrefix, + cookiesOptions, + ], + ) + + const value = useMemo( + () => ({ + destinations, + needConsent, + isDestinationsLoaded, + categoriesConsent: cookieConsent, + saveConsent, + categories: CATEGORIES, + }), + [ + destinations, + isDestinationsLoaded, + needConsent, + cookieConsent, + saveConsent, + ], + ) + + return ( + + {children} + + ) +} diff --git a/packages/use-analytics/src/cookies-consent/index.ts b/packages/use-analytics/src/cookies-consent/index.ts new file mode 100644 index 000000000..11ab1440e --- /dev/null +++ b/packages/use-analytics/src/cookies-consent/index.ts @@ -0,0 +1,5 @@ +export { + CookieConsentProvider, + useCookieConsent, +} from './CookieConsentProvider' +// export { SegmentConsentMiddleware } from './SegmentConsentMiddleware' diff --git a/packages/use-analytics/src/helpers/array.ts b/packages/use-analytics/src/helpers/array.ts new file mode 100644 index 000000000..7aedb4a98 --- /dev/null +++ b/packages/use-analytics/src/helpers/array.ts @@ -0,0 +1 @@ +export const uniq = (array: T[]): T[] => [...new Set(array)] diff --git a/packages/use-analytics/src/helpers/isClient.ts b/packages/use-analytics/src/helpers/isClient.ts new file mode 100644 index 000000000..2e4e2d452 --- /dev/null +++ b/packages/use-analytics/src/helpers/isClient.ts @@ -0,0 +1 @@ +export const IS_CLIENT = typeof document !== 'undefined' diff --git a/packages/use-analytics/src/helpers/misc.ts b/packages/use-analytics/src/helpers/misc.ts new file mode 100644 index 000000000..3029d3034 --- /dev/null +++ b/packages/use-analytics/src/helpers/misc.ts @@ -0,0 +1,2 @@ +export const stringToHash = (str: string): number => + Array.from(str).reduce((s, c) => Math.imul(31, s) + c.charCodeAt(0) || 0, 0) diff --git a/packages/use-analytics/src/index.ts b/packages/use-analytics/src/index.ts new file mode 100644 index 000000000..45f6c4ea6 --- /dev/null +++ b/packages/use-analytics/src/index.ts @@ -0,0 +1,7 @@ +export type { + Analytics, + OnEventError, + AnalyticsProviderProps, +} from './analytics/useAnalytics' + +export { AnalyticsProvider, useAnalytics, useDestinations } from './analytics' diff --git a/packages/use-analytics/src/types.ts b/packages/use-analytics/src/types.ts new file mode 100644 index 000000000..c110df95b --- /dev/null +++ b/packages/use-analytics/src/types.ts @@ -0,0 +1,71 @@ +import { CATEGORIES } from './constants' + +export type CategoryKind = (typeof CATEGORIES)[number] + +export const isCategoryKind = (key: string): key is CategoryKind => + CATEGORIES.includes(key as CategoryKind) + +// Uniton type +type Provider = 'custom' +type ResolutionStrategy = 'and' | 'or' + +type Consents = { consent: CategoryKind }[] + +type Destination = { + id: string + name: string + enabled: boolean + config: { + siteID: string + blacklistedEvents: string[] + whitelistedEvents: string[] + eventFilteringOption: 'blacklistedEvents' | 'whitelistedEvents' + consentManagement: { + provider: Provider + resolutionStrategy: ResolutionStrategy + consents: Consents + }[] + } + destinationDefinitionId: string + destinationDefinition: { + name: string + displayName: string + } + updatedAt?: string + shouldApplyDeviceModeTransformation: boolean + propagateEventsUntransformedOnError: boolean +} + +export type AnalyticsConfig = { + source: { + id: string + name: string + writeKey: string + config: Record + enabled: boolean + workspaceId: string + destinations: Destination[] + } +} + +export type AnalyticsIntegration = { + consents: CategoryKind[] + name: string + displayName: string +} + +export type Consent = { [K in CategoryKind]: boolean } + +export type Integration = { + category: CategoryKind + name: string +} + +export type Integrations = Integration[] + +export type Config = { + analytics?: { + writeKey: string + cdnURL: string + } | null +} diff --git a/packages/use-analytics/tsconfig.build.json b/packages/use-analytics/tsconfig.build.json new file mode 100644 index 000000000..744c16721 --- /dev/null +++ b/packages/use-analytics/tsconfig.build.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "emitDeclarationOnly": true, + "rootDir": "src", + "outDir": "dist" + }, + "exclude": [ + "*.config.ts", + "*.setup.ts", + "**/__tests__", + "**/__mocks__", + "src/**/*.test.tsx" + ] +} diff --git a/packages/use-analytics/tsconfig.json b/packages/use-analytics/tsconfig.json new file mode 100644 index 000000000..7c2b0759a --- /dev/null +++ b/packages/use-analytics/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts", "src/**/*.tsx", "*.config.ts"] +} diff --git a/packages/use-analytics/vite.config.ts b/packages/use-analytics/vite.config.ts new file mode 100644 index 000000000..68b0a8a78 --- /dev/null +++ b/packages/use-analytics/vite.config.ts @@ -0,0 +1,21 @@ +import { defineConfig, mergeConfig } from 'vite' +import { defaultConfig } from '../../vite.config' +import { defaultConfig as vitestDefaultConfig } from '../../vitest.config' + +const config = { + ...defineConfig(defaultConfig), + ...vitestDefaultConfig, +} + +export default mergeConfig(config, { + build: { + lib: { + formats: ['es', 'cjs'], + entry: ['src/index.ts', 'src/cookies-consent/index.ts'], + }, + }, + test: { + environment: 'jsdom', + setupFiles: ['./vitest.setup.ts'], + }, +}) diff --git a/packages/use-analytics/vitest.setup.ts b/packages/use-analytics/vitest.setup.ts new file mode 100644 index 000000000..b49124c8b --- /dev/null +++ b/packages/use-analytics/vitest.setup.ts @@ -0,0 +1,11 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ +import * as matchers from '@testing-library/jest-dom/matchers' +import '@testing-library/jest-dom/vitest' +import { cleanup } from '@testing-library/react' +import { afterEach, expect } from 'vitest' + +expect.extend(matchers) + +afterEach(() => { + cleanup() +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7404889f..311b49f21 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,8 +43,8 @@ catalogs: specifier: 11.12.0 version: 11.12.0 '@eslint/compat': - specifier: 1.3.0 - version: 1.3.0 + specifier: 1.2.9 + version: 1.2.9 '@eslint/eslintrc': specifier: 3.3.1 version: 3.3.1 @@ -60,6 +60,9 @@ catalogs: '@growthbook/growthbook-react': specifier: 1.5.1 version: 1.5.1 + '@rudderstack/analytics-js': + specifier: ^3.20.1 + version: 3.20.1 '@segment/analytics-next': specifier: 1.81.0 version: 1.81.0 @@ -76,8 +79,8 @@ catalogs: specifier: 29.5.14 version: 29.5.14 '@types/node': - specifier: 22.15.31 - version: 22.15.31 + specifier: 22.15.30 + version: 22.15.30 '@types/react': specifier: 19.1.8 version: 19.1.8 @@ -85,17 +88,17 @@ catalogs: specifier: 19.1.6 version: 19.1.6 '@typescript-eslint/eslint-plugin': - specifier: 8.34.0 - version: 8.34.0 + specifier: 8.33.1 + version: 8.33.1 '@typescript-eslint/parser': - specifier: 8.34.0 - version: 8.34.0 + specifier: 8.33.1 + version: 8.33.1 '@vitejs/plugin-react': specifier: 4.5.2 version: 4.5.2 '@vitest/coverage-istanbul': - specifier: 3.2.4 - version: 3.2.4 + specifier: 3.2.3 + version: 3.2.3 browserslist: specifier: 4.25.0 version: 4.25.0 @@ -250,13 +253,16 @@ importers: version: 2.29.4 '@commitlint/cli': specifier: 'catalog:' - version: 19.8.1(@types/node@22.15.31)(typescript@5.8.3) + version: 19.8.1(@types/node@22.15.30)(typescript@5.8.3) '@commitlint/config-conventional': specifier: 'catalog:' version: 19.8.1 '@eslint/eslintrc': specifier: 'catalog:' version: 3.3.1 + '@rudderstack/analytics-js': + specifier: 'catalog:' + version: 3.20.1 '@scaleway/eslint-config-react': specifier: workspace:* version: link:packages/eslint-config-react @@ -274,7 +280,7 @@ importers: version: 29.5.14 '@types/node': specifier: 'catalog:' - version: 22.15.31 + version: 22.15.30 '@types/react': specifier: 'catalog:' version: 19.1.8 @@ -283,10 +289,10 @@ importers: version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': specifier: 'catalog:' - version: 4.5.2(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(terser@5.37.0)(yaml@2.8.0)) + version: 4.5.2(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(terser@5.37.0)(yaml@2.8.0)) '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 3.2.4(vitest@3.2.4(@types/node@22.15.31)(happy-dom@17.6.3)(jiti@2.4.2)(jsdom@20.0.3)(terser@5.37.0)(yaml@2.8.0)) + version: 3.2.3(vitest@3.2.4(@types/node@22.15.30)(happy-dom@17.6.3)(jiti@2.4.2)(jsdom@20.0.3)(terser@5.37.0)(yaml@2.8.0)) browserslist: specifier: 'catalog:' version: 4.25.0 @@ -331,13 +337,13 @@ importers: version: 5.8.3 vite: specifier: 'catalog:' - version: 6.3.5(@types/node@22.15.31)(jiti@2.4.2)(terser@5.37.0)(yaml@2.8.0) + version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(terser@5.37.0)(yaml@2.8.0) vitest: specifier: 'catalog:' - version: 3.2.4(@types/node@22.15.31)(happy-dom@17.6.3)(jiti@2.4.2)(jsdom@20.0.3)(terser@5.37.0)(yaml@2.8.0) + version: 3.2.4(@types/node@22.15.30)(happy-dom@17.6.3)(jiti@2.4.2)(jsdom@20.0.3)(terser@5.37.0)(yaml@2.8.0) vitest-localstorage-mock: specifier: 'catalog:' - version: 0.1.2(vitest@3.2.4(@types/node@22.15.31)(happy-dom@17.6.3)(jiti@2.4.2)(jsdom@20.0.3)(terser@5.37.0)(yaml@2.8.0)) + version: 0.1.2(vitest@3.2.4(@types/node@22.15.30)(happy-dom@17.6.3)(jiti@2.4.2)(jsdom@20.0.3)(terser@5.37.0)(yaml@2.8.0)) wait-for-expect: specifier: 'catalog:' version: 3.0.2 @@ -368,7 +374,7 @@ importers: version: 11.12.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) '@eslint/compat': specifier: 'catalog:' - version: 1.3.0(eslint@9.28.0(jiti@2.4.2)) + version: 1.2.9(eslint@9.28.0(jiti@2.4.2)) '@eslint/eslintrc': specifier: 'catalog:' version: 3.3.1 @@ -377,10 +383,10 @@ importers: version: 4.4.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/eslint-plugin': specifier: 'catalog:' - version: 8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + version: 8.33.1(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/parser': specifier: 'catalog:' - version: 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + version: 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) eslint-config-airbnb: specifier: 'catalog:' version: 19.0.4(eslint-plugin-import@2.31.0)(eslint-plugin-jsx-a11y@6.10.2(eslint@9.28.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.2.0(eslint@9.28.0(jiti@2.4.2)))(eslint-plugin-react@7.37.5(eslint@9.28.0(jiti@2.4.2)))(eslint@9.28.0(jiti@2.4.2)) @@ -395,7 +401,7 @@ importers: version: 3.2.0(eslint@9.28.0(jiti@2.4.2)) eslint-plugin-import: specifier: 'catalog:' - version: 2.31.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@4.4.3)(eslint@9.28.0(jiti@2.4.2)) + version: 2.31.0(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@4.4.3)(eslint@9.28.0(jiti@2.4.2)) eslint-plugin-jsx-a11y: specifier: 'catalog:' version: 6.10.2(eslint@9.28.0(jiti@2.4.2)) @@ -421,6 +427,25 @@ importers: packages/tsconfig: {} + packages/use-analytics: + dependencies: + '@rudderstack/analytics-js': + specifier: 'catalog:' + version: 3.20.1 + '@segment/analytics-next': + specifier: 'catalog:' + version: 1.81.0 + cookie: + specifier: 'catalog:' + version: 1.0.2 + use-deep-compare-effect: + specifier: 'catalog:' + version: 1.8.1(react@19.1.0) + devDependencies: + react: + specifier: 'catalog:' + version: 19.1.0 + packages/use-dataloader: devDependencies: react: @@ -1629,8 +1654,8 @@ packages: resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/compat@1.3.0': - resolution: {integrity: sha512-ZBygRBqpDYiIHsN+d1WyHn3TYgzgpzLEcgJUxTATyiInQbKZz6wZb6+ljwdg8xeeOe4v03z6Uh6lELiw0/mVhQ==} + '@eslint/compat@1.2.9': + resolution: {integrity: sha512-gCdSY54n7k+driCadyMNv8JSPzYLeDVM/ikZRtvtROBpRdFSkS8W9A82MqsaY7lZuwL0wiapgD0NT1xT0hyJsA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^9.10.0 @@ -1904,6 +1929,9 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@rudderstack/analytics-js@3.20.1': + resolution: {integrity: sha512-iaY2RGsR/vCBOPzYx23fmLfUS/vP+/wo3koN2N5mHFAolXVBTXjnvNo6fsgeUcvRd7NFdcKUFZA36PLP8RwmqQ==} + '@segment/analytics-core@1.8.1': resolution: {integrity: sha512-EYcdBdhfi1pOYRX+Sf5orpzzYYFmDHTEu6+w0hjXpW5bWkWct+Nv6UJg1hF4sGDKEQjpZIinLTpQ4eioFM4KeQ==} @@ -2019,8 +2047,8 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/node@22.15.31': - resolution: {integrity: sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==} + '@types/node@22.15.30': + resolution: {integrity: sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -2045,16 +2073,16 @@ packages: '@types/yargs@17.0.32': resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} - '@typescript-eslint/eslint-plugin@8.34.0': - resolution: {integrity: sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==} + '@typescript-eslint/eslint-plugin@8.33.1': + resolution: {integrity: sha512-TDCXj+YxLgtvxvFlAvpoRv9MAncDLBV2oT9Bd7YBGC/b/sEURoOYuIwLI99rjWOfY3QtDzO+mk0n4AmdFExW8A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.34.0 + '@typescript-eslint/parser': ^8.33.1 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/parser@8.34.0': - resolution: {integrity: sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==} + '@typescript-eslint/parser@8.33.1': + resolution: {integrity: sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -2066,12 +2094,6 @@ packages: peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/project-service@8.34.0': - resolution: {integrity: sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/scope-manager@5.62.0': resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2080,10 +2102,6 @@ packages: resolution: {integrity: sha512-dM4UBtgmzHR9bS0Rv09JST0RcHYearoEoo3pG5B6GoTR9XcyeqX87FEhPo+5kTvVfKCvfHaHrcgeJQc6mrDKrA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.34.0': - resolution: {integrity: sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.33.1': resolution: {integrity: sha512-STAQsGYbHCF0/e+ShUQ4EatXQ7ceh3fBCXkNU7/MZVKulrlq1usH7t2FhxvCpuCi5O5oi1vmVaAjrGeL71OK1g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2096,8 +2114,8 @@ packages: peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/type-utils@8.34.0': - resolution: {integrity: sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg==} + '@typescript-eslint/type-utils@8.33.1': + resolution: {integrity: sha512-1cG37d9xOkhlykom55WVwG2QRNC7YXlxMaMzqw2uPeJixBFfKWZgaP/hjAObqMN/u3fr5BrTwTnc31/L9jQ2ww==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -2130,12 +2148,6 @@ packages: peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/typescript-estree@8.34.0': - resolution: {integrity: sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@5.62.0': resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2149,13 +2161,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@8.34.0': - resolution: {integrity: sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/visitor-keys@5.62.0': resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2164,10 +2169,6 @@ packages: resolution: {integrity: sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.34.0': - resolution: {integrity: sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@unrs/resolver-binding-darwin-arm64@1.7.11': resolution: {integrity: sha512-i3/wlWjQJXMh1uiGtiv7k1EYvrrS3L1hdwmWJJiz1D8jWy726YFYPIxQWbEIVPVAgrfRR0XNlLrTQwq17cuCGw==} cpu: [arm64] @@ -2259,10 +2260,10 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 - '@vitest/coverage-istanbul@3.2.4': - resolution: {integrity: sha512-IDlpuFJiWU9rhcKLkpzj8mFu/lpe64gVgnV15ZOrYx1iFzxxrxCzbExiUEKtwwXRvEiEMUS6iZeYgnMxgbqbxQ==} + '@vitest/coverage-istanbul@3.2.3': + resolution: {integrity: sha512-kW1n4neEJbMYcAjzk+fS1nKReP+gURgaQ1/KzjAZsDyaUklnUjuWn38tLNbSwoobUBquuvdE6EzljSRxOuDXOQ==} peerDependencies: - vitest: 3.2.4 + vitest: 3.2.3 '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} @@ -5818,11 +5819,11 @@ snapshots: human-id: 4.1.1 prettier: 2.8.8 - '@commitlint/cli@19.8.1(@types/node@22.15.31)(typescript@5.8.3)': + '@commitlint/cli@19.8.1(@types/node@22.15.30)(typescript@5.8.3)': dependencies: '@commitlint/format': 19.8.1 '@commitlint/lint': 19.8.1 - '@commitlint/load': 19.8.1(@types/node@22.15.31)(typescript@5.8.3) + '@commitlint/load': 19.8.1(@types/node@22.15.30)(typescript@5.8.3) '@commitlint/read': 19.8.1 '@commitlint/types': 19.8.1 tinyexec: 1.0.1 @@ -5869,7 +5870,7 @@ snapshots: '@commitlint/rules': 19.8.1 '@commitlint/types': 19.8.1 - '@commitlint/load@19.8.1(@types/node@22.15.31)(typescript@5.8.3)': + '@commitlint/load@19.8.1(@types/node@22.15.30)(typescript@5.8.3)': dependencies: '@commitlint/config-validator': 19.8.1 '@commitlint/execute-rule': 19.8.1 @@ -5877,7 +5878,7 @@ snapshots: '@commitlint/types': 19.8.1 chalk: 5.4.1 cosmiconfig: 9.0.0(typescript@5.8.3) - cosmiconfig-typescript-loader: 6.1.0(@types/node@22.15.31)(cosmiconfig@9.0.0(typescript@5.8.3))(typescript@5.8.3) + cosmiconfig-typescript-loader: 6.1.0(@types/node@22.15.30)(cosmiconfig@9.0.0(typescript@5.8.3))(typescript@5.8.3) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -6111,7 +6112,7 @@ snapshots: '@eslint-community/regexpp@4.12.1': {} - '@eslint/compat@1.3.0(eslint@9.28.0(jiti@2.4.2))': + '@eslint/compat@1.2.9(eslint@9.28.0(jiti@2.4.2))': optionalDependencies: eslint: 9.28.0(jiti@2.4.2) @@ -6224,7 +6225,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.15.31 + '@types/node': 22.15.30 '@types/yargs': 17.0.32 chalk: 4.1.2 @@ -6373,6 +6374,8 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@rudderstack/analytics-js@3.20.1': {} + '@segment/analytics-core@1.8.1': dependencies: '@lukeed/uuid': 2.0.1 @@ -6505,7 +6508,7 @@ snapshots: '@types/conventional-commits-parser@5.0.0': dependencies: - '@types/node': 22.15.31 + '@types/node': 22.15.30 '@types/deep-eql@4.0.2': {} @@ -6532,7 +6535,7 @@ snapshots: '@types/node@12.20.55': {} - '@types/node@22.15.31': + '@types/node@22.15.30': dependencies: undici-types: 6.21.0 @@ -6556,14 +6559,14 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.33.1(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.34.0 - '@typescript-eslint/type-utils': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.34.0 + '@typescript-eslint/parser': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.33.1 + '@typescript-eslint/type-utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.33.1 eslint: 9.28.0(jiti@2.4.2) graphemer: 1.4.0 ignore: 7.0.3 @@ -6573,12 +6576,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@typescript-eslint/scope-manager': 8.34.0 - '@typescript-eslint/types': 8.34.0 - '@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.34.0 + '@typescript-eslint/scope-manager': 8.33.1 + '@typescript-eslint/types': 8.33.1 + '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.33.1 debug: 4.4.1 eslint: 9.28.0(jiti@2.4.2) typescript: 5.8.3 @@ -6586,15 +6589,6 @@ snapshots: - supports-color '@typescript-eslint/project-service@8.33.1(typescript@5.8.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.33.1(typescript@5.8.3) - '@typescript-eslint/types': 8.33.1 - debug: 4.4.1 - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/project-service@8.34.0(typescript@5.8.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.34.0(typescript@5.8.3) '@typescript-eslint/types': 8.34.0 @@ -6613,11 +6607,6 @@ snapshots: '@typescript-eslint/types': 8.33.1 '@typescript-eslint/visitor-keys': 8.33.1 - '@typescript-eslint/scope-manager@8.34.0': - dependencies: - '@typescript-eslint/types': 8.34.0 - '@typescript-eslint/visitor-keys': 8.34.0 - '@typescript-eslint/tsconfig-utils@8.33.1(typescript@5.8.3)': dependencies: typescript: 5.8.3 @@ -6626,10 +6615,10 @@ snapshots: dependencies: typescript: 5.8.3 - '@typescript-eslint/type-utils@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3) - '@typescript-eslint/utils': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) + '@typescript-eslint/utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) debug: 4.4.1 eslint: 9.28.0(jiti@2.4.2) ts-api-utils: 2.1.0(typescript@5.8.3) @@ -6673,22 +6662,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.34.0(typescript@5.8.3)': - dependencies: - '@typescript-eslint/project-service': 8.34.0(typescript@5.8.3) - '@typescript-eslint/tsconfig-utils': 8.34.0(typescript@5.8.3) - '@typescript-eslint/types': 8.34.0 - '@typescript-eslint/visitor-keys': 8.34.0 - debug: 4.4.1 - fast-glob: 3.3.3 - is-glob: 4.0.3 - minimatch: 9.0.4 - semver: 7.7.1 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/utils@5.62.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@9.28.0(jiti@2.4.2)) @@ -6715,17 +6688,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': - dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0(jiti@2.4.2)) - '@typescript-eslint/scope-manager': 8.34.0 - '@typescript-eslint/types': 8.34.0 - '@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3) - eslint: 9.28.0(jiti@2.4.2) - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/visitor-keys@5.62.0': dependencies: '@typescript-eslint/types': 5.62.0 @@ -6736,11 +6698,6 @@ snapshots: '@typescript-eslint/types': 8.33.1 eslint-visitor-keys: 4.2.0 - '@typescript-eslint/visitor-keys@8.34.0': - dependencies: - '@typescript-eslint/types': 8.34.0 - eslint-visitor-keys: 4.2.0 - '@unrs/resolver-binding-darwin-arm64@1.7.11': optional: true @@ -6794,7 +6751,7 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.7.11': optional: true - '@vitejs/plugin-react@4.5.2(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(terser@5.37.0)(yaml@2.8.0))': + '@vitejs/plugin-react@4.5.2(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(terser@5.37.0)(yaml@2.8.0))': dependencies: '@babel/core': 7.27.4 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.4) @@ -6802,11 +6759,11 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.11 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(@types/node@22.15.31)(jiti@2.4.2)(terser@5.37.0)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(terser@5.37.0)(yaml@2.8.0) transitivePeerDependencies: - supports-color - '@vitest/coverage-istanbul@3.2.4(vitest@3.2.4(@types/node@22.15.31)(happy-dom@17.6.3)(jiti@2.4.2)(jsdom@20.0.3)(terser@5.37.0)(yaml@2.8.0))': + '@vitest/coverage-istanbul@3.2.3(vitest@3.2.4(@types/node@22.15.30)(happy-dom@17.6.3)(jiti@2.4.2)(jsdom@20.0.3)(terser@5.37.0)(yaml@2.8.0))': dependencies: '@istanbuljs/schema': 0.1.3 debug: 4.4.1 @@ -6818,7 +6775,7 @@ snapshots: magicast: 0.3.5 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@22.15.31)(happy-dom@17.6.3)(jiti@2.4.2)(jsdom@20.0.3)(terser@5.37.0)(yaml@2.8.0) + vitest: 3.2.4(@types/node@22.15.30)(happy-dom@17.6.3)(jiti@2.4.2)(jsdom@20.0.3)(terser@5.37.0)(yaml@2.8.0) transitivePeerDependencies: - supports-color @@ -6830,13 +6787,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(terser@5.37.0)(yaml@2.8.0))': + '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(terser@5.37.0)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.5(@types/node@22.15.31)(jiti@2.4.2)(terser@5.37.0)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(terser@5.37.0)(yaml@2.8.0) '@vitest/pretty-format@3.2.4': dependencies: @@ -7202,9 +7159,9 @@ snapshots: dependencies: browserslist: 4.25.0 - cosmiconfig-typescript-loader@6.1.0(@types/node@22.15.31)(cosmiconfig@9.0.0(typescript@5.8.3))(typescript@5.8.3): + cosmiconfig-typescript-loader@6.1.0(@types/node@22.15.30)(cosmiconfig@9.0.0(typescript@5.8.3))(typescript@5.8.3): dependencies: - '@types/node': 22.15.31 + '@types/node': 22.15.30 cosmiconfig: 9.0.0(typescript@5.8.3) jiti: 2.4.2 typescript: 5.8.3 @@ -7579,7 +7536,7 @@ snapshots: dependencies: confusing-browser-globals: 1.0.11 eslint: 9.28.0(jiti@2.4.2) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@4.4.3)(eslint@9.28.0(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@4.4.3)(eslint@9.28.0(jiti@2.4.2)) object.assign: 4.1.7 object.entries: 1.1.8 semver: 6.3.1 @@ -7588,7 +7545,7 @@ snapshots: dependencies: eslint: 9.28.0(jiti@2.4.2) eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.31.0)(eslint@9.28.0(jiti@2.4.2)) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@4.4.3)(eslint@9.28.0(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@4.4.3)(eslint@9.28.0(jiti@2.4.2)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.28.0(jiti@2.4.2)) eslint-plugin-react: 7.37.5(eslint@9.28.0(jiti@2.4.2)) eslint-plugin-react-hooks: 5.2.0(eslint@9.28.0(jiti@2.4.2)) @@ -7625,15 +7582,15 @@ snapshots: tinyglobby: 0.2.14 unrs-resolver: 1.7.11 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@4.4.3)(eslint@9.28.0(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@4.4.3)(eslint@9.28.0(jiti@2.4.2)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.3)(eslint@9.28.0(jiti@2.4.2)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.3)(eslint@9.28.0(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) eslint: 9.28.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 4.4.3(eslint-plugin-import@2.31.0)(eslint@9.28.0(jiti@2.4.2)) @@ -7646,7 +7603,7 @@ snapshots: eslint: 9.28.0(jiti@2.4.2) ignore: 5.3.1 - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@4.4.3)(eslint@9.28.0(jiti@2.4.2)): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@4.4.3)(eslint@9.28.0(jiti@2.4.2)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -7657,7 +7614,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.28.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.3)(eslint@9.28.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.3)(eslint@9.28.0(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -7669,7 +7626,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -8371,7 +8328,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.15.31 + '@types/node': 22.15.30 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -9514,7 +9471,7 @@ snapshots: use-deep-compare-effect@1.8.1(react@19.1.0): dependencies: - '@babel/runtime': 7.26.0 + '@babel/runtime': 7.26.9 dequal: 2.0.3 react: 19.1.0 @@ -9523,13 +9480,13 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - vite-node@3.2.4(@types/node@22.15.31)(jiti@2.4.2)(terser@5.37.0)(yaml@2.8.0): + vite-node@3.2.4(@types/node@22.15.30)(jiti@2.4.2)(terser@5.37.0)(yaml@2.8.0): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.5(@types/node@22.15.31)(jiti@2.4.2)(terser@5.37.0)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(terser@5.37.0)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -9544,7 +9501,7 @@ snapshots: - tsx - yaml - vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(terser@5.37.0)(yaml@2.8.0): + vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(terser@5.37.0)(yaml@2.8.0): dependencies: esbuild: 0.25.0 fdir: 6.4.4(picomatch@4.0.2) @@ -9553,21 +9510,21 @@ snapshots: rollup: 4.40.0 tinyglobby: 0.2.13 optionalDependencies: - '@types/node': 22.15.31 + '@types/node': 22.15.30 fsevents: 2.3.3 jiti: 2.4.2 terser: 5.37.0 yaml: 2.8.0 - vitest-localstorage-mock@0.1.2(vitest@3.2.4(@types/node@22.15.31)(happy-dom@17.6.3)(jiti@2.4.2)(jsdom@20.0.3)(terser@5.37.0)(yaml@2.8.0)): + vitest-localstorage-mock@0.1.2(vitest@3.2.4(@types/node@22.15.30)(happy-dom@17.6.3)(jiti@2.4.2)(jsdom@20.0.3)(terser@5.37.0)(yaml@2.8.0)): dependencies: - vitest: 3.2.4(@types/node@22.15.31)(happy-dom@17.6.3)(jiti@2.4.2)(jsdom@20.0.3)(terser@5.37.0)(yaml@2.8.0) + vitest: 3.2.4(@types/node@22.15.30)(happy-dom@17.6.3)(jiti@2.4.2)(jsdom@20.0.3)(terser@5.37.0)(yaml@2.8.0) - vitest@3.2.4(@types/node@22.15.31)(happy-dom@17.6.3)(jiti@2.4.2)(jsdom@20.0.3)(terser@5.37.0)(yaml@2.8.0): + vitest@3.2.4(@types/node@22.15.30)(happy-dom@17.6.3)(jiti@2.4.2)(jsdom@20.0.3)(terser@5.37.0)(yaml@2.8.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(terser@5.37.0)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(terser@5.37.0)(yaml@2.8.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -9585,11 +9542,11 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@22.15.31)(jiti@2.4.2)(terser@5.37.0)(yaml@2.8.0) - vite-node: 3.2.4(@types/node@22.15.31)(jiti@2.4.2)(terser@5.37.0)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(terser@5.37.0)(yaml@2.8.0) + vite-node: 3.2.4(@types/node@22.15.30)(jiti@2.4.2)(terser@5.37.0)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.15.31 + '@types/node': 22.15.30 happy-dom: 17.6.3 jsdom: 20.0.3 transitivePeerDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 90bfb1c3e..a09784adf 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,36 +1,38 @@ packages: - packages/* + catalog: - "@babel/core": 7.27.4 - "@babel/eslint-parser": 7.27.5 - "@babel/plugin-transform-runtime": 7.27.4 - "@babel/preset-env": 7.27.2 - "@babel/preset-react": 7.27.1 - "@babel/preset-typescript": 7.27.1 - "@biomejs/biome": 1.9.4 - "@changesets/changelog-github": 0.5.1 - "@changesets/cli": 2.29.4 - "@commitlint/cli": 19.8.1 - "@commitlint/config-conventional": 19.8.1 - "@emotion/eslint-plugin": 11.12.0 - "@eslint/compat": 1.3.0 - "@eslint/eslintrc": 3.3.1 - "@formatjs/ecma402-abstract": 2.3.4 - "@formatjs/fast-memoize": 2.2.7 - "@formatjs/icu-messageformat-parser": 2.11.2 - "@growthbook/growthbook-react": 1.5.1 - "@segment/analytics-next": 1.81.0 - "@stylistic/eslint-plugin": 4.4.1 - "@testing-library/jest-dom": 6.6.3 - "@testing-library/react": 16.3.0 - "@types/jest": 29.5.14 - "@types/node": 22.15.31 - "@types/react": 19.1.8 - "@types/react-dom": 19.1.6 - "@typescript-eslint/eslint-plugin": 8.34.0 - "@typescript-eslint/parser": 8.34.0 - "@vitejs/plugin-react": 4.5.2 - "@vitest/coverage-istanbul": 3.2.4 + '@babel/core': 7.27.4 + '@babel/eslint-parser': 7.27.5 + '@babel/plugin-transform-runtime': 7.27.4 + '@babel/preset-env': 7.27.2 + '@babel/preset-react': 7.27.1 + '@babel/preset-typescript': 7.27.1 + '@biomejs/biome': 1.9.4 + '@changesets/changelog-github': 0.5.1 + '@changesets/cli': 2.29.4 + '@commitlint/cli': 19.8.1 + '@commitlint/config-conventional': 19.8.1 + '@emotion/eslint-plugin': 11.12.0 + '@eslint/compat': 1.2.9 + '@eslint/eslintrc': 3.3.1 + '@formatjs/ecma402-abstract': 2.3.4 + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/icu-messageformat-parser': 2.11.2 + '@growthbook/growthbook-react': 1.5.1 + '@rudderstack/analytics-js': ^3.20.1 + '@segment/analytics-next': 1.81.0 + '@stylistic/eslint-plugin': 4.4.1 + '@testing-library/jest-dom': 6.6.3 + '@testing-library/react': 16.3.0 + '@types/jest': 29.5.14 + '@types/node': 22.15.30 + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6 + '@typescript-eslint/eslint-plugin': 8.33.1 + '@typescript-eslint/parser': 8.33.1 + '@vitejs/plugin-react': 4.5.2 + '@vitest/coverage-istanbul': 3.2.3 browserslist: 4.25.0 builtin-modules: 5.0.0 cookie: 1.0.2 @@ -71,3 +73,5 @@ catalog: vitest: 3.2.4 vitest-localstorage-mock: 0.1.2 wait-for-expect: 3.0.2 + +catalogMode: strict