@convex-internal/web-analytics 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,197 @@
1
+ # @convex-internal/web-analytics
2
+
3
+ Shared consent state, consent banner, and analytics setup for Convex websites.
4
+ Wrap the app in `WebAnalyticsProvider`. It renders the consent banner. Wrap
5
+ other tracking, such as GA or an ad pixel, in `Consented` so it loads only
6
+ after consent is confirmed. Use `useConsent` when you need the consent status
7
+ itself.
8
+
9
+ ## Usage
10
+
11
+ Install the package, then render `WebAnalyticsProvider` at the root of the
12
+ app. It is a client component. `useConsent` throws if called outside that
13
+ provider.
14
+
15
+ The provider starts analytics in production, except on localhost, syncs
16
+ consent to PostHog, and shows the consent banner when the visitor has not
17
+ chosen yet. Pass `posthogConfig` for site-specific options, such as the
18
+ website's `dub_id` campaign param:
19
+
20
+ ```tsx
21
+ <WebAnalyticsProvider posthogConfig={{ custom_campaign_params: ["dub_id"] }}>
22
+ {children}
23
+ </WebAnalyticsProvider>
24
+ ```
25
+
26
+ ### `capture`
27
+
28
+ Record an event from client code. The optional third argument is
29
+ [PostHog's capture options](https://posthog.com/docs/libraries/js/usage).
30
+
31
+ ```tsx
32
+ import { capture } from "@convex-internal/web-analytics";
33
+
34
+ capture("www-login", undefined, {
35
+ transport: "sendBeacon",
36
+ send_instantly: true,
37
+ });
38
+ ```
39
+
40
+ ### Consent banner
41
+
42
+ The provider shows the banner when `consent` is `"undecided"`. It records
43
+ `cookie_consent_shown` when it appears and `cookie_consent_decided` with
44
+ `{ decision: "accepted" | "declined" }` just before the choice is saved.
45
+
46
+ The banner stays hidden while Global Privacy Control is on, because that
47
+ resolves `consent` to `"declined"` rather than `"undecided"`. GPC does not
48
+ write the cookie; see [Cookie behavior](#cookie-behavior).
49
+
50
+ Theme with `--wa-banner-*` variables on `.wa-banner`:
51
+
52
+ ```css
53
+ .wa-banner {
54
+ --wa-banner-background-color: var(--color-seashell);
55
+ --wa-banner-font-family: var(--font-primary);
56
+ --wa-banner-accept-background-color: var(--color-plum-p4);
57
+ }
58
+ ```
59
+
60
+ - Background, text, and border: `--wa-banner-background-color`,
61
+ `--wa-banner-text-color`, `--wa-banner-border-color`,
62
+ `--wa-banner-border-radius`, `--wa-banner-box-shadow`,
63
+ `--wa-banner-backdrop-filter`
64
+ - Type and layout: `--wa-banner-font-family`, `--wa-banner-font-size`,
65
+ `--wa-banner-padding`, `--wa-banner-max-width`, `--wa-banner-action-gap`,
66
+ `--wa-banner-z-index`
67
+ - Link and focus: `--wa-banner-link-color`, `--wa-banner-focus-color`
68
+ - Buttons: `--wa-banner-button-border-radius`, `--wa-banner-button-padding`,
69
+ `--wa-banner-button-min-height`, `--wa-banner-button-font-size`,
70
+ `--wa-banner-button-font-weight`, `--wa-banner-button-line-height`
71
+ - Accept button: `--wa-banner-accept-background-color`, `--wa-banner-accept-text-color`,
72
+ `--wa-banner-accept-hover-background-color`
73
+ - Decline button: `--wa-banner-decline-background-color`,
74
+ `--wa-banner-decline-text-color`, `--wa-banner-decline-hover-background-color`
75
+
76
+ ### `Consented`
77
+
78
+ Renders children only when `consent` is `"consented"`. It renders nothing during
79
+ SSR, while consent is still `"unknown"` or `"undecided"`, if the visitor
80
+ declined, or while Global Privacy Control is on.
81
+
82
+ ```tsx
83
+ import { Consented } from "@convex-internal/web-analytics";
84
+
85
+ <Consented>
86
+ <YourAnalyticsScripts />
87
+ </Consented>;
88
+ ```
89
+
90
+ ### `useConsent`
91
+
92
+ - `consent` (`ConsentStatus`)
93
+ - `"unknown"` until cookie has been read (SSR and first client render)
94
+ - `"undecided"` after that if there is no recognized choice yet
95
+ - `"consented"` if the cookie is `true` and GPC is not set
96
+ - `"declined"` if the cookie is `false` or GPC is set
97
+ - `setConsent("consented" | "declined")`
98
+ - Saves the choice to the `allowsCookies` cookie and updates context
99
+ - Do not write the cookie yourself
100
+
101
+ Use the hook when you need `consent` or `setConsent` directly. Enable tracking
102
+ only when `consent === "consented"`.
103
+
104
+ ### Cookie behavior
105
+
106
+ Once the cookie has been read, `consent` is resolved in this order:
107
+
108
+ 1. **Global Privacy Control.** If `navigator.globalPrivacyControl === true`,
109
+ `consent` is `"declined"`. GPC is not stored: the cookie is not written or
110
+ changed. Turn GPC off and the stored cookie (if set) applies again.
111
+ 2. **Cookie.** `true` is `"consented"`, `false` is `"declined"`, and a missing
112
+ or unrecognized value is `"undecided"`.
113
+
114
+ The cookie is stored with `path=/` and a max-age of one year. On `convex.dev`
115
+ and `*.convex.dev` it is also set with `domain=.convex.dev` so the website,
116
+ Docs, Stack, and other subdomains share one choice. The cookie is marked
117
+ `Secure` everywhere except localhost.
118
+
119
+ Changes made through `setConsent` update immediately. Changes made
120
+ elsewhere, such as in another tab or DevTools, are synchronized through cookie
121
+ change events when available, and when the window regains focus or its
122
+ visibility changes as a fallback.
123
+
124
+ ## Test in-development changes
125
+
126
+ 1. In the `web-analytics` repo, build and pack the package:
127
+
128
+ ```bash
129
+ cd /path/to/web-analytics
130
+ npm run build
131
+ npm pack
132
+ ```
133
+
134
+ This creates a tarball such as `convex-internal-web-analytics-1.0.0.tgz`.
135
+
136
+ 1. Install that tarball in the consuming app:
137
+
138
+ ```bash
139
+ cd /path/to/consuming-app
140
+ npm install /path/to/web-analytics/convex-internal-web-analytics-1.0.0.tgz --force
141
+ ```
142
+
143
+ `--force` ensures npm refreshes the package when the filename and version have
144
+ not changed.
145
+
146
+ ## Publish a version to npm
147
+
148
+ 1. Confirm that you are signed in to the npm account with access to the
149
+ `@convex-internal` organization:
150
+
151
+ ```bash
152
+ npm whoami
153
+ ```
154
+
155
+ Run `npm login` first if needed.
156
+
157
+ 1. Commit any README or code changes, then bump the version. `npm version`
158
+ requires a clean Git worktree and creates a version commit and tag.
159
+
160
+ ```bash
161
+ npm version major # breaking, or 0.x → 1.0.0
162
+ npm version minor # new backwards-compatible functionality
163
+ npm version patch # bug fixes
164
+ ```
165
+
166
+ 1. Run tests, inspect the package contents, then publish. `.npmrc` sets
167
+ `ignore-scripts=true`, so `prepack` will not run — build first:
168
+
169
+ ```bash
170
+ npm test
171
+ npm run build
172
+ npm pack --dry-run
173
+ npm publish --access public
174
+ ```
175
+
176
+ If npm 2FA is enabled, add `--otp=<code>`.
177
+
178
+ ## Consume a published version
179
+
180
+ Convex repos set `min-release-age=7` in `.npmrc`, so a version published in
181
+ the last 7 days will fail to install (including `npm ci` in CI). Override
182
+ the window for the install, then exclude this package so later installs
183
+ succeed:
184
+
185
+ ```bash
186
+ npm install @convex-internal/web-analytics@<version> --min-release-age=0
187
+ ```
188
+
189
+ ```
190
+ # consuming-app/.npmrc
191
+ min-release-age-exclude[]=@convex-internal/web-analytics
192
+ ```
193
+
194
+ You can drop the exclude once this version has been on npm for 7 days.
195
+
196
+ Use the local tarball workflow whenever you need to test changes that have
197
+ not been published yet.
@@ -0,0 +1,2 @@
1
+ import "./banner.css";
2
+ export declare function ConsentBanner(): import("react").JSX.Element | null;
@@ -0,0 +1,31 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef } from "react";
4
+ import { capture } from "./capture.js";
5
+ import { useConsent } from "./useConsent.js";
6
+ import "./banner.css";
7
+ export function ConsentBanner() {
8
+ const { consent, setConsent } = useConsent();
9
+ const shouldShow = consent === "undecided";
10
+ const wasShowing = useRef(false);
11
+ useEffect(() => {
12
+ if (shouldShow && !wasShowing.current) {
13
+ // Captures once when the banner becomes visible, not on later renders.
14
+ capture("cookie_consent_shown");
15
+ }
16
+ wasShowing.current = shouldShow;
17
+ }, [shouldShow]);
18
+ if (!shouldShow) {
19
+ return null;
20
+ }
21
+ const decide = (decision) => {
22
+ try {
23
+ // Captures before the cookie write so decline is captured cookieless.
24
+ capture("cookie_consent_decided", { decision });
25
+ }
26
+ finally {
27
+ setConsent(decision === "accepted" ? "consented" : "declined");
28
+ }
29
+ };
30
+ return (_jsxs("aside", { "aria-label": "Privacy choices", className: "wa-banner", children: [_jsx("p", { className: "wa-banner-text", children: "We use cookies and similar technologies to understand how people interact with our site." }), _jsxs("p", { className: "wa-banner-text wa-banner-policy", children: ["See our", " ", _jsx("a", { className: "wa-banner-link", href: "https://www.convex.dev/legal/privacy", rel: "noopener noreferrer", target: "_blank", children: "Privacy Policy" }), " ", "to learn more."] }), _jsxs("div", { className: "wa-banner-actions", children: [_jsx("button", { className: "wa-banner-button wa-banner-decline", onClick: () => decide("declined"), type: "button", children: "Decline" }), _jsx("button", { className: "wa-banner-button wa-banner-accept", onClick: () => decide("accepted"), type: "button", children: "Accept" })] })] }));
31
+ }
@@ -0,0 +1,5 @@
1
+ import type { ReactNode } from "react";
2
+ export interface ConsentedProps {
3
+ children: ReactNode;
4
+ }
5
+ export declare function Consented({ children }: ConsentedProps): ReactNode;
@@ -0,0 +1,9 @@
1
+ "use client";
2
+ import { useConsent } from "./useConsent.js";
3
+ export function Consented({ children }) {
4
+ const { consent } = useConsent();
5
+ if (consent !== "consented") {
6
+ return null;
7
+ }
8
+ return children;
9
+ }
@@ -0,0 +1,22 @@
1
+ import { type PostHogConfig } from "posthog-js";
2
+ import { type ReactNode } from "react";
3
+ export type { PostHogConfig };
4
+ /**
5
+ * `"unknown"` until the cookie has been read on the client (SSR and first
6
+ * render). `"undecided"` when there is no recognized choice yet. `"consented"`
7
+ * when the cookie is `true` and Global Privacy Control (GPC) is not set.
8
+ * `"declined"` when the cookie is `false`, or while GPC is set.
9
+ *
10
+ * GPC does not write or change the cookie. If it is later turned off, the
11
+ * stored cookie (or the banner, if none) applies again.
12
+ */
13
+ export type ConsentStatus = "unknown" | "undecided" | "consented" | "declined";
14
+ export interface WebAnalyticsProviderProps {
15
+ children: ReactNode;
16
+ posthogConfig?: Partial<PostHogConfig>;
17
+ }
18
+ export declare const ConsentContext: import("react").Context<{
19
+ consent: ConsentStatus;
20
+ setConsent: (status: "consented" | "declined") => void;
21
+ } | null>;
22
+ export declare function WebAnalyticsProvider({ children, posthogConfig, }: WebAnalyticsProviderProps): import("react").JSX.Element;
@@ -0,0 +1,88 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { posthog } from "posthog-js";
4
+ import { PostHogProvider } from "posthog-js/react";
5
+ import { createContext, useEffect } from "react";
6
+ import { ConsentBanner } from "./ConsentBanner.js";
7
+ import { useBooleanCookie } from "./useCookie.js";
8
+ const COOKIE_NAME = "allowsCookies";
9
+ const POSTHOG_KEY = "phc_JDNTRxeh2li2sQTRO0IcOYMJcp8fPs5nTK9TU751nQK";
10
+ const POSTHOG_HOST = "https://ap.convex.dev";
11
+ function resolveConsent(isReady, cookieValue, gpcOptOut) {
12
+ if (!isReady) {
13
+ return "unknown";
14
+ }
15
+ if (gpcOptOut) {
16
+ return "declined";
17
+ }
18
+ if (cookieValue === true) {
19
+ return "consented";
20
+ }
21
+ if (cookieValue === false) {
22
+ return "declined";
23
+ }
24
+ return "undecided";
25
+ }
26
+ export const ConsentContext = createContext(null);
27
+ export function WebAnalyticsProvider({ children, posthogConfig, }) {
28
+ const { value, isReady, setValue } = useBooleanCookie(COOKIE_NAME);
29
+ const gpcOptOut = typeof navigator !== "undefined" && navigator.globalPrivacyControl === true;
30
+ const consent = resolveConsent(isReady, value, gpcOptOut);
31
+ useEffect(() => {
32
+ // Prevent PostHog from initializing in development or on localhost.
33
+ if (process.env.NODE_ENV !== "production" ||
34
+ window.location.hostname === "localhost" ||
35
+ window.location.hostname === "127.0.0.1" ||
36
+ window.location.hostname === "[::1]") {
37
+ return;
38
+ }
39
+ // If PostHog is already initialized, update its config.
40
+ if (posthog.__loaded) {
41
+ if (posthogConfig) {
42
+ posthog.set_config(posthogConfig);
43
+ }
44
+ return;
45
+ }
46
+ // Initialize PostHog.
47
+ posthog.init(POSTHOG_KEY, {
48
+ api_host: POSTHOG_HOST,
49
+ ui_host: "https://us.posthog.com/",
50
+ // Automatically capture navigations using the History API.
51
+ capture_pageview: "history_change",
52
+ // Together, these two settings capture cookieless events before the user
53
+ // chooses and continue doing so if they decline. Accepting enables persistent
54
+ // tracking. See more: https://posthog.com/tutorials/cookieless-tracking
55
+ opt_out_capturing_by_default: true,
56
+ cookieless_mode: "on_reject",
57
+ // Site-specific options to add or override the defaults.
58
+ ...posthogConfig,
59
+ });
60
+ }, [posthogConfig]);
61
+ // One-way sync: make PostHog match the resolved consent status.
62
+ useEffect(() => {
63
+ // No decision yet or PostHog is not initialized.
64
+ if (consent === "unknown" || consent === "undecided" || !posthog.__loaded) {
65
+ return;
66
+ }
67
+ const posthogConsent = posthog.get_explicit_consent_status();
68
+ const targetConsent = consent === "consented" ? "granted" : "denied";
69
+ if (posthogConsent === targetConsent) {
70
+ return;
71
+ }
72
+ if (consent === "consented") {
73
+ // Records an `$opt_in` event, which is distinct from ConsentBanner's
74
+ // `cookie_consent_decided` as it also fires when we sync an existing
75
+ // allowsCookies cookie for returning visitors.
76
+ posthog.opt_in_capturing();
77
+ }
78
+ else {
79
+ posthog.opt_out_capturing();
80
+ }
81
+ }, [consent]);
82
+ return (_jsxs(ConsentContext.Provider, { value: {
83
+ consent,
84
+ setConsent: (status) => {
85
+ setValue(status === "consented" ? "true" : "false");
86
+ },
87
+ }, children: [_jsx(PostHogProvider, { client: posthog, children: children }), _jsx(ConsentBanner, {})] }));
88
+ }
@@ -0,0 +1,82 @@
1
+ .wa-banner {
2
+ box-sizing: border-box;
3
+ position: fixed;
4
+ z-index: var(--wa-banner-z-index, 50);
5
+ inset-inline: 1rem;
6
+ bottom: 1rem;
7
+ padding: var(--wa-banner-padding, 1rem);
8
+ border: 1px solid var(--wa-banner-border-color, black);
9
+ border-radius: var(--wa-banner-border-radius, 0);
10
+ background-color: var(--wa-banner-background-color, white);
11
+ box-shadow: var(--wa-banner-box-shadow, none);
12
+ -webkit-backdrop-filter: var(--wa-banner-backdrop-filter, none);
13
+ backdrop-filter: var(--wa-banner-backdrop-filter, none);
14
+ color: var(--wa-banner-text-color, black);
15
+ font-family: var(--wa-banner-font-family, inherit);
16
+ font-size: var(--wa-banner-font-size, 1rem);
17
+
18
+ .wa-banner-text {
19
+ margin: 0 0 0.5rem;
20
+ }
21
+
22
+ .wa-banner-policy {
23
+ margin-bottom: 1rem;
24
+ }
25
+
26
+ .wa-banner-link {
27
+ color: var(--wa-banner-link-color, inherit);
28
+ text-decoration: underline;
29
+ text-underline-offset: 0.125rem;
30
+ }
31
+
32
+ .wa-banner-actions {
33
+ display: flex;
34
+ justify-content: flex-end;
35
+ gap: var(--wa-banner-action-gap, 0.5rem);
36
+ }
37
+
38
+ .wa-banner-button {
39
+ box-sizing: border-box;
40
+ min-height: var(--wa-banner-button-min-height, 2.75rem);
41
+ padding: var(--wa-banner-button-padding, 0.5rem 0.75rem);
42
+ border: 0;
43
+ border-radius: var(--wa-banner-button-border-radius, 0);
44
+ font-family: inherit;
45
+ font-size: var(--wa-banner-button-font-size, inherit);
46
+ font-weight: var(--wa-banner-button-font-weight, inherit);
47
+ line-height: var(--wa-banner-button-line-height, inherit);
48
+ cursor: pointer;
49
+ transition: background-color 0.15s;
50
+
51
+ &:focus-visible {
52
+ outline: 2px solid var(--wa-banner-focus-color, black);
53
+ outline-offset: 2px;
54
+ }
55
+
56
+ &.wa-banner-decline {
57
+ background-color: var(--wa-banner-decline-background-color, lightgray);
58
+ color: var(--wa-banner-decline-text-color, black);
59
+
60
+ &:hover {
61
+ background-color: var(
62
+ --wa-banner-decline-hover-background-color,
63
+ lightgray
64
+ );
65
+ }
66
+ }
67
+
68
+ &.wa-banner-accept {
69
+ background-color: var(--wa-banner-accept-background-color, black);
70
+ color: var(--wa-banner-accept-text-color, white);
71
+
72
+ &:hover {
73
+ background-color: var(--wa-banner-accept-hover-background-color, black);
74
+ }
75
+ }
76
+ }
77
+
78
+ @media (min-width: 40rem) {
79
+ inset-inline-start: auto;
80
+ max-width: var(--wa-banner-max-width, 24rem);
81
+ }
82
+ }
@@ -0,0 +1,3 @@
1
+ import { type CaptureOptions } from "posthog-js";
2
+ export type { CaptureOptions };
3
+ export declare function capture(event: string, properties?: Record<string, unknown>, options?: CaptureOptions): void;
@@ -0,0 +1,5 @@
1
+ "use client";
2
+ import { posthog } from "posthog-js";
3
+ export function capture(event, properties, options) {
4
+ posthog.capture(event, properties, options);
5
+ }
@@ -0,0 +1,2 @@
1
+ export declare function getCookie(name: string): string | undefined;
2
+ export declare function setCookie(name: string, value: string): void;
package/dist/cookie.js ADDED
@@ -0,0 +1,23 @@
1
+ const MAX_AGE = 365 * 24 * 60 * 60;
2
+ export function getCookie(name) {
3
+ const row = document.cookie
4
+ .split("; ")
5
+ .find((part) => part.startsWith(`${name}=`));
6
+ if (!row) {
7
+ return undefined;
8
+ }
9
+ return row.slice(name.length + 1);
10
+ }
11
+ export function setCookie(name, value) {
12
+ const hostname = window.location.hostname;
13
+ const parts = [`${name}=${value}`, "path=/", `max-age=${MAX_AGE}`];
14
+ // Share cookies across the website, Docs, Stack, etc. When making changes
15
+ // to domain or cookie names, update those projects as well.
16
+ if (hostname === "convex.dev" || hostname.endsWith(".convex.dev")) {
17
+ parts.push("domain=.convex.dev");
18
+ }
19
+ if (hostname !== "localhost") {
20
+ parts.push("secure");
21
+ }
22
+ document.cookie = parts.join("; ");
23
+ }
@@ -0,0 +1,4 @@
1
+ export { WebAnalyticsProvider, type WebAnalyticsProviderProps, type PostHogConfig, type ConsentStatus, } from "./WebAnalyticsProvider.js";
2
+ export { Consented, type ConsentedProps } from "./Consented.js";
3
+ export { useConsent } from "./useConsent.js";
4
+ export { capture, type CaptureOptions } from "./capture.js";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { WebAnalyticsProvider, } from "./WebAnalyticsProvider.js";
2
+ export { Consented } from "./Consented.js";
3
+ export { useConsent } from "./useConsent.js";
4
+ export { capture } from "./capture.js";
@@ -0,0 +1,4 @@
1
+ export declare function useConsent(): {
2
+ consent: import("./WebAnalyticsProvider.js").ConsentStatus;
3
+ setConsent: (status: "consented" | "declined") => void;
4
+ };
@@ -0,0 +1,10 @@
1
+ "use client";
2
+ import { useContext } from "react";
3
+ import { ConsentContext } from "./WebAnalyticsProvider.js";
4
+ export function useConsent() {
5
+ const context = useContext(ConsentContext);
6
+ if (context === null) {
7
+ throw new Error("useConsent must be used within WebAnalyticsProvider");
8
+ }
9
+ return context;
10
+ }
@@ -0,0 +1,10 @@
1
+ export declare function useCookie<T extends string = string>(name: string): {
2
+ value: string | undefined;
3
+ setValue: (next: T) => void;
4
+ isReady: boolean;
5
+ };
6
+ export declare function useBooleanCookie(name: string): {
7
+ value: boolean | undefined;
8
+ isReady: boolean;
9
+ setValue: (next: "false" | "true") => void;
10
+ };
@@ -0,0 +1,38 @@
1
+ "use client";
2
+ import { useCallback, useEffect, useState } from "react";
3
+ import { getCookie, setCookie } from "./cookie.js";
4
+ export function useCookie(name) {
5
+ const [value, setValueState] = useState(undefined);
6
+ const [isReady, setIsReady] = useState(false);
7
+ useEffect(() => {
8
+ const sync = () => {
9
+ setValueState(getCookie(name));
10
+ setIsReady(true);
11
+ };
12
+ const handleCookieChange = (event) => {
13
+ const changed = [...event.changed, ...event.deleted].some((cookie) => cookie.name === name);
14
+ if (changed) {
15
+ sync();
16
+ }
17
+ };
18
+ sync();
19
+ window.addEventListener("focus", sync);
20
+ document.addEventListener("visibilitychange", sync);
21
+ window.cookieStore?.addEventListener("change", handleCookieChange);
22
+ return () => {
23
+ window.removeEventListener("focus", sync);
24
+ document.removeEventListener("visibilitychange", sync);
25
+ window.cookieStore?.removeEventListener("change", handleCookieChange);
26
+ };
27
+ }, [name]);
28
+ const setValue = useCallback((next) => {
29
+ setCookie(name, next);
30
+ setValueState(next);
31
+ }, [name]);
32
+ return { value, setValue, isReady };
33
+ }
34
+ export function useBooleanCookie(name) {
35
+ const { value: raw, isReady, setValue } = useCookie(name);
36
+ const value = raw === "true" ? true : raw === "false" ? false : undefined;
37
+ return { value, isReady, setValue };
38
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@convex-internal/web-analytics",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "default": "./dist/index.js"
9
+ },
10
+ "./banner.css": "./dist/banner.css"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc && cp src/banner.css dist/banner.css",
17
+ "prepack": "npm run build",
18
+ "test": "vitest run"
19
+ },
20
+ "sideEffects": [
21
+ "**/*.css"
22
+ ],
23
+ "dependencies": {
24
+ "posthog-js": "^1.211.0"
25
+ },
26
+ "peerDependencies": {
27
+ "react": "^18.0.0 || ^19.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "@testing-library/react": "^16.3.2",
31
+ "@types/node": "^26.2.0",
32
+ "@types/react": "^19.2.18",
33
+ "@types/react-dom": "^19.2.4",
34
+ "jsdom": "^30.0.1",
35
+ "react": "^19.2.4",
36
+ "react-dom": "^19.2.8",
37
+ "typescript": "^5.9.2",
38
+ "vitest": "^4.1.10"
39
+ }
40
+ }