@djangocfg/analytics 2.1.489 → 2.1.490
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 +46 -0
- package/package.json +2 -2
- package/src/core/client.ts +5 -1
- package/src/core/global.ts +52 -0
- package/src/core/index.ts +2 -0
- package/src/events.ts +53 -3
- package/src/index.ts +11 -1
- package/src/react/context.tsx +9 -2
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @djangocfg/analytics
|
|
2
|
+
|
|
3
|
+
First-party product analytics for DjangoCFG. One client fans out pageviews and
|
|
4
|
+
custom events to django-cfg and optional GA4 transports.
|
|
5
|
+
|
|
6
|
+
## Product events
|
|
7
|
+
|
|
8
|
+
Mount `AnalyticsProvider` once (Layouts `BaseApp` does this automatically),
|
|
9
|
+
then use the typed React API:
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { useAnalytics } from '@djangocfg/analytics'
|
|
13
|
+
|
|
14
|
+
const { event } = useAnalytics()
|
|
15
|
+
event('layouts.auth.step_viewed', { step: 'otp' })
|
|
16
|
+
event('billing.checkout_started', { plan: 'pro' })
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
For browser integration code outside React, use the optional facade installed
|
|
20
|
+
by the provider. It is the same client — not a second transport:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
window.analytics?.event('billing.checkout_started', { plan: 'pro' })
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Unknown product event names are accepted. Extend `AnalyticsEventMap` to make
|
|
27
|
+
your product's names and properties type-safe everywhere:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
declare module '@djangocfg/analytics' {
|
|
31
|
+
interface AnalyticsEventMap {
|
|
32
|
+
'billing.checkout_started': { plan: 'pro' | 'team' }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Use stable event names as your analytics contract. Django goals and funnels are
|
|
38
|
+
configured against those names; do not encode dynamic identifiers in names —
|
|
39
|
+
put bounded, non-sensitive dimensions in `props` instead.
|
|
40
|
+
|
|
41
|
+
## Identity and funnels
|
|
42
|
+
|
|
43
|
+
Browser journeys are scoped to a host. A property can aggregate hosts, but it
|
|
44
|
+
does not merge anonymous visitor identities across them. Server-confirmed auth
|
|
45
|
+
events use the request-derived identity for their originating host, so an auth
|
|
46
|
+
funnel on that host can join its UI and successful-login events safely.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/analytics",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.490",
|
|
4
4
|
"description": "First-party analytics for django-cfg apps: one route listener, N transports (self-hosted + GA4). Pageviews, sessions, and authenticated-user attribution.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"django",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
}
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
66
|
+
"@djangocfg/typescript-config": "^2.1.490",
|
|
67
67
|
"@types/node": "^25.9.5",
|
|
68
68
|
"@types/react": "19.2.15",
|
|
69
69
|
"next": "^15.5.9",
|
package/src/core/client.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import type { AnalyticsEventPayload, AnalyticsOptions, AnalyticsTransport } from './types'
|
|
17
|
+
import type { AnalyticsEventName, AnalyticsEventProps } from '../events'
|
|
17
18
|
|
|
18
19
|
export class AnalyticsClient {
|
|
19
20
|
private transports: AnalyticsTransport[] = []
|
|
@@ -70,7 +71,10 @@ export class AnalyticsClient {
|
|
|
70
71
|
this.fanOut((t) => t.pageview(payload))
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
trackEvent
|
|
74
|
+
trackEvent<Name extends AnalyticsEventName>(
|
|
75
|
+
name: Name,
|
|
76
|
+
props: AnalyticsEventProps<Name> = {} as AnalyticsEventProps<Name>,
|
|
77
|
+
): void {
|
|
74
78
|
if (typeof window === 'undefined') return
|
|
75
79
|
|
|
76
80
|
this.fanOut((t) =>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Imperative access to the one AnalyticsClient mounted by AnalyticsProvider.
|
|
3
|
+
*
|
|
4
|
+
* `window.analytics` is deliberately a thin optional facade, analogous to
|
|
5
|
+
* `window.dialog` but not a second event pipeline. React code uses
|
|
6
|
+
* `useAnalytics`; integration code without a React tree can use
|
|
7
|
+
* `getAnalytics()?.event(...)`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { AnalyticsEventName, AnalyticsEventProps } from '../events'
|
|
11
|
+
import type { AnalyticsClient } from './client'
|
|
12
|
+
|
|
13
|
+
export interface AnalyticsAPI {
|
|
14
|
+
event<Name extends AnalyticsEventName>(
|
|
15
|
+
name: Name,
|
|
16
|
+
props?: AnalyticsEventProps<Name>,
|
|
17
|
+
): void
|
|
18
|
+
flush(): void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let activeApi: AnalyticsAPI | null = null
|
|
22
|
+
|
|
23
|
+
export function getAnalytics(): AnalyticsAPI | null {
|
|
24
|
+
return activeApi
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function installAnalytics(client: AnalyticsClient): () => void {
|
|
28
|
+
const api: AnalyticsAPI = {
|
|
29
|
+
event: (name, props) => client.trackEvent(name, props),
|
|
30
|
+
flush: () => client.flush(),
|
|
31
|
+
}
|
|
32
|
+
activeApi = api
|
|
33
|
+
|
|
34
|
+
if (typeof window !== 'undefined') {
|
|
35
|
+
window.analytics = api
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return () => {
|
|
39
|
+
if (activeApi !== api) return
|
|
40
|
+
activeApi = null
|
|
41
|
+
if (typeof window !== 'undefined' && window.analytics === api) {
|
|
42
|
+
delete window.analytics
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
declare global {
|
|
48
|
+
interface Window {
|
|
49
|
+
/** Installed only while an AnalyticsProvider is mounted. */
|
|
50
|
+
analytics?: AnalyticsAPI
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/core/index.ts
CHANGED
package/src/events.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Product-event catalog.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* This is the single TypeScript source of truth for framework event names and
|
|
5
|
+
* their properties. Products extend `AnalyticsEventMap` by declaration merging
|
|
6
|
+
* instead of maintaining a second enum, transport, or browser global.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -58,3 +59,52 @@ export const AnalyticsEvent = {
|
|
|
58
59
|
|
|
59
60
|
export type AnalyticsCategoryType = typeof AnalyticsCategory[keyof typeof AnalyticsCategory];
|
|
60
61
|
export type AnalyticsEventType = typeof AnalyticsEvent[keyof typeof AnalyticsEvent];
|
|
62
|
+
|
|
63
|
+
export type AnalyticsPropertyValue = string | number | boolean | null;
|
|
64
|
+
export type AnalyticsEventProperties = Record<string, AnalyticsPropertyValue>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Framework event names with their valid payloads.
|
|
68
|
+
*
|
|
69
|
+
* Apps can extend this interface:
|
|
70
|
+
*
|
|
71
|
+
* ```ts
|
|
72
|
+
* declare module '@djangocfg/analytics' {
|
|
73
|
+
* interface AnalyticsEventMap {
|
|
74
|
+
* 'billing.checkout_started': { plan: 'pro' | 'team' }
|
|
75
|
+
* }
|
|
76
|
+
* }
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export interface AnalyticsEventMap {
|
|
80
|
+
auth_otp_request: { method?: 'email' | 'phone' };
|
|
81
|
+
auth_login_success: { provider?: 'email' | 'github'; is_new_user?: boolean };
|
|
82
|
+
auth_otp_verify_fail: { reason?: string };
|
|
83
|
+
auth_session_expired: Record<string, never>;
|
|
84
|
+
auth_token_refresh: Record<string, never>;
|
|
85
|
+
auth_token_refresh_fail: { reason?: string };
|
|
86
|
+
auth_logout: Record<string, never>;
|
|
87
|
+
auth_oauth_start: { provider: 'github' };
|
|
88
|
+
auth_oauth_fail: { provider?: 'github'; reason?: string };
|
|
89
|
+
error_boundary: { boundary?: string };
|
|
90
|
+
error_api: { status?: number; operation?: string };
|
|
91
|
+
error_validation: { field?: string };
|
|
92
|
+
error_network: { operation?: string };
|
|
93
|
+
nav_admin_enter: Record<string, never>;
|
|
94
|
+
nav_dashboard_enter: Record<string, never>;
|
|
95
|
+
nav_page_view: Record<string, never>;
|
|
96
|
+
theme_change: { theme?: string };
|
|
97
|
+
sidebar_toggle: { open?: boolean };
|
|
98
|
+
mobile_menu_open: Record<string, never>;
|
|
99
|
+
user_profile_view: Record<string, never>;
|
|
100
|
+
user_profile_update: Record<string, never>;
|
|
101
|
+
'layouts.auth.step_viewed': { step: 'identifier' | 'otp' | '2fa' | '2fa-setup' | 'success' };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A catalog event is fully typed; arbitrary product events remain supported. */
|
|
105
|
+
export type AnalyticsEventName = keyof AnalyticsEventMap | (string & {});
|
|
106
|
+
|
|
107
|
+
export type AnalyticsEventProps<Name extends AnalyticsEventName> =
|
|
108
|
+
Name extends keyof AnalyticsEventMap
|
|
109
|
+
? AnalyticsEventMap[Name]
|
|
110
|
+
: AnalyticsEventProperties;
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
export { AnalyticsClient } from './core/client'
|
|
19
|
+
export { getAnalytics } from './core/global'
|
|
20
|
+
export type { AnalyticsAPI } from './core/global'
|
|
19
21
|
export type {
|
|
20
22
|
AnalyticsEventPayload,
|
|
21
23
|
AnalyticsOptions,
|
|
@@ -34,4 +36,12 @@ export { PageviewTracker } from './next/PageviewTracker'
|
|
|
34
36
|
export type { PageviewTrackerProps } from './next/PageviewTracker'
|
|
35
37
|
|
|
36
38
|
export { AnalyticsCategory, AnalyticsEvent } from './events'
|
|
37
|
-
export type {
|
|
39
|
+
export type {
|
|
40
|
+
AnalyticsCategoryType,
|
|
41
|
+
AnalyticsEventMap,
|
|
42
|
+
AnalyticsEventName,
|
|
43
|
+
AnalyticsEventProps,
|
|
44
|
+
AnalyticsEventProperties,
|
|
45
|
+
AnalyticsEventType,
|
|
46
|
+
AnalyticsPropertyValue,
|
|
47
|
+
} from './events'
|
package/src/react/context.tsx
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
import { createContext, useContext, useEffect, useMemo, useRef, type ReactNode } from 'react'
|
|
9
9
|
|
|
10
10
|
import { AnalyticsClient } from '../core/client'
|
|
11
|
+
import { installAnalytics } from '../core/global'
|
|
11
12
|
import type { AnalyticsOptions } from '../core/types'
|
|
13
|
+
import type { AnalyticsEventName, AnalyticsEventProps } from '../events'
|
|
12
14
|
|
|
13
15
|
const AnalyticsContext = createContext<AnalyticsClient | null>(null)
|
|
14
16
|
|
|
@@ -41,7 +43,11 @@ export function AnalyticsProvider({
|
|
|
41
43
|
|
|
42
44
|
useEffect(() => {
|
|
43
45
|
void client.init()
|
|
44
|
-
|
|
46
|
+
const uninstall = installAnalytics(client)
|
|
47
|
+
return () => {
|
|
48
|
+
uninstall()
|
|
49
|
+
client.destroy()
|
|
50
|
+
}
|
|
45
51
|
}, [client])
|
|
46
52
|
|
|
47
53
|
useEffect(() => {
|
|
@@ -63,7 +69,8 @@ export function useAnalytics() {
|
|
|
63
69
|
|
|
64
70
|
return useMemo(
|
|
65
71
|
() => ({
|
|
66
|
-
event: (name:
|
|
72
|
+
event: <Name extends AnalyticsEventName>(name: Name, props?: AnalyticsEventProps<Name>) =>
|
|
73
|
+
client?.trackEvent(name, props),
|
|
67
74
|
identify: (userId: string | null) => client?.identify(userId),
|
|
68
75
|
flush: () => client?.flush(false),
|
|
69
76
|
/** False when no AnalyticsProvider is mounted above this component. */
|