@djangocfg/layouts 2.1.470 → 2.1.472

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "2.1.470",
3
+ "version": "2.1.472",
4
4
  "description": "Simple, straightforward layout components for Next.js - import and use with props",
5
5
  "keywords": [
6
6
  "layouts",
@@ -89,11 +89,12 @@
89
89
  "check": "tsc --noEmit"
90
90
  },
91
91
  "peerDependencies": {
92
- "@djangocfg/api": "^2.1.470",
93
- "@djangocfg/centrifugo": "^2.1.470",
94
- "@djangocfg/devtools": "^2.1.470",
95
- "@djangocfg/i18n": "^2.1.470",
96
- "@djangocfg/ui-core": "^2.1.470",
92
+ "@djangocfg/analytics": "workspace:*",
93
+ "@djangocfg/api": "^2.1.472",
94
+ "@djangocfg/centrifugo": "^2.1.472",
95
+ "@djangocfg/devtools": "^2.1.472",
96
+ "@djangocfg/i18n": "^2.1.472",
97
+ "@djangocfg/ui-core": "^2.1.472",
97
98
  "@hookform/resolvers": "^5.2.2",
98
99
  "consola": "^3.4.2",
99
100
  "lucide-react": "^0.545.0",
@@ -120,17 +121,17 @@
120
121
  "libphonenumber-js": "^1.13.8",
121
122
  "nextjs-toploader": "^3.9.17",
122
123
  "qrcode.react": "^4.2.0",
123
- "react-ga4": "^2.1.0",
124
124
  "uuid": "^11.1.1"
125
125
  },
126
126
  "devDependencies": {
127
- "@djangocfg/api": "^2.1.470",
128
- "@djangocfg/centrifugo": "^2.1.470",
129
- "@djangocfg/devtools": "^2.1.470",
130
- "@djangocfg/i18n": "^2.1.470",
131
- "@djangocfg/typescript-config": "^2.1.470",
132
- "@djangocfg/ui-core": "^2.1.470",
133
- "@djangocfg/ui-tools": "^2.1.470",
127
+ "@djangocfg/analytics": "workspace:*",
128
+ "@djangocfg/api": "^2.1.472",
129
+ "@djangocfg/centrifugo": "^2.1.472",
130
+ "@djangocfg/devtools": "^2.1.472",
131
+ "@djangocfg/i18n": "^2.1.472",
132
+ "@djangocfg/typescript-config": "^2.1.472",
133
+ "@djangocfg/ui-core": "^2.1.472",
134
+ "@djangocfg/ui-tools": "^2.1.472",
134
135
  "@types/node": "^25.9.5",
135
136
  "@types/react": "19.2.15",
136
137
  "@types/react-dom": "19.2.3",
@@ -1,34 +1,120 @@
1
+ 'use client';
2
+
1
3
  /**
2
- * AnalyticsProvider Component
4
+ * The layouts adapter for `@djangocfg/analytics`.
5
+ *
6
+ * BaseApp mounts this inside AuthProvider, which is what lets it attach the
7
+ * signed-in user's id — the thing hosted analytics (Plausible, Umami, GA4
8
+ * without extra plumbing) structurally cannot do.
3
9
  *
4
- * Initializes Google Analytics and auto-tracks pageviews.
5
- * Can work standalone with trackingId prop, or use AppContext if available.
10
+ * Transports are composed here:
11
+ * - `djangocfg` always on. Same-origin, self-hosted, no third party.
12
+ * - `ga4` — only when a tracking id is configured.
13
+ *
14
+ * Note the behavior change from the old implementation: that one hard-gated on
15
+ * `isProd`, so it was completely silent in development and you could not tell
16
+ * whether tracking worked until you shipped. Self-hosting removes the reason for
17
+ * that gate — your dev traffic goes to your own database.
6
18
  */
7
19
 
8
- 'use client';
20
+ import { ReactNode, useEffect, useMemo } from 'react';
9
21
 
10
- import { ReactNode } from 'react';
22
+ import {
23
+ AnalyticsProvider as BaseAnalyticsProvider,
24
+ PageviewTracker,
25
+ djangocfg,
26
+ ga4,
27
+ useAnalyticsClient,
28
+ type AnalyticsTransport,
29
+ } from '@djangocfg/analytics';
30
+ import { setAnalyticsSink, useAuth } from '@djangocfg/api/auth';
31
+ import { useLocaleOptional } from '@djangocfg/i18n';
11
32
 
12
- import { useAnalytics } from './useAnalytics';
33
+ import { usePathnameWithoutLocale } from '../../hooks/usePathnameWithoutLocale';
13
34
 
14
35
  interface AnalyticsProviderProps {
15
36
  children: ReactNode;
16
- /** Google Analytics tracking ID (optional if using AppContext) */
37
+ /** GA4 measurement ID. Omit to run first-party only. */
17
38
  trackingId?: string;
39
+ /**
40
+ * Templated route (`/[locale]/blog/[slug]`). Next does not expose it to client
41
+ * components, so an app can pass it for exact grouping. Optional: without it
42
+ * the locale-stripped pathname is used, which already prevents the worst
43
+ * failure (see below).
44
+ */
45
+ route?: string;
46
+ }
47
+
48
+ export function AnalyticsProvider({
49
+ children,
50
+ trackingId,
51
+ route,
52
+ }: AnalyticsProviderProps) {
53
+ const { user } = useAuth();
54
+
55
+ // The locale is READ here rather than passed in by every app.
56
+ //
57
+ // It matters: without it, `/en/pricing` and `/ru/pricing` are counted as two
58
+ // different pages, and the top-pages report is meaningless on a
59
+ // locale-prefixed site. `useLocaleOptional` returns undefined (rather than
60
+ // throwing) in an app with no i18n, so this stays safe for both.
61
+ // `?? undefined` is load-bearing: useLocaleOptional returns `null` with no
62
+ // I18nProvider, and this package builds with `strict: false`, so a raw null
63
+ // would sail through the `locale?: string` prop and end up in the payload.
64
+ const locale = useLocaleOptional() ?? undefined;
65
+
66
+ // The locale-stripped path is the fallback "route". Not as good as the real
67
+ // templated route (`/blog/[slug]` still fragments per slug), but it fixes the
68
+ // locale split with zero work from the app.
69
+ const strippedPath = usePathnameWithoutLocale(locale);
70
+
71
+ const transports = useMemo<AnalyticsTransport[]>(() => {
72
+ const list: AnalyticsTransport[] = [djangocfg()];
73
+ if (trackingId) list.push(ga4({ trackingId }));
74
+ return list;
75
+ }, [trackingId]);
76
+
77
+ return (
78
+ <BaseAnalyticsProvider
79
+ transports={transports}
80
+ // An opaque id — never an email. Goes back to null on logout, or the next
81
+ // anonymous session would inherit it.
82
+ userId={user?.id != null ? String(user.id) : null}
83
+ >
84
+ <AuthEventBridge />
85
+ <PageviewTracker route={route ?? strippedPath} locale={locale} />
86
+ {children}
87
+ </BaseAnalyticsProvider>
88
+ );
18
89
  }
19
90
 
20
91
  /**
21
- * Analytics Provider that initializes tracking
22
- * Automatically:
23
- * - Initializes GA4 with tracking ID from prop or config
24
- * - Sets user ID when authenticated
25
- * - Tracks page views on route changes
92
+ * Routes `@djangocfg/api`'s auth events into the analytics client.
93
+ *
94
+ * `api` cannot import `analytics` (analytics reads auth state that would be a
95
+ * cycle), so `api` exposes a sink and we install the real implementation here,
96
+ * where both packages are already in scope.
97
+ *
98
+ * Without this bridge, every auth event — AUTH_LOGIN_SUCCESS, AUTH_OAUTH_FAIL,
99
+ * AUTH_SESSION_EXPIRED — hits api's built-in no-op stub and is silently
100
+ * discarded. That was the status quo before this package existed.
26
101
  */
27
- export function AnalyticsProvider({ children, trackingId }: AnalyticsProviderProps) {
28
- // If trackingId is provided as prop, use it directly
29
- // Otherwise, useAnalytics will try to get it from AppContext
30
- useAnalytics(trackingId);
31
- return <>{children}</>;
102
+ function AuthEventBridge() {
103
+ const client = useAnalyticsClient();
104
+
105
+ useEffect(() => {
106
+ if (!client) return;
107
+
108
+ setAnalyticsSink({
109
+ event: (name, params) => client.trackEvent(name, params),
110
+ setUser: (userId) => client.identify(userId),
111
+ });
112
+
113
+ // Uninstall on unmount, or a torn-down client keeps receiving events.
114
+ return () => setAnalyticsSink(null);
115
+ }, [client]);
116
+
117
+ return null;
32
118
  }
33
119
 
34
120
  export default AnalyticsProvider;
@@ -1,11 +1,19 @@
1
1
  /**
2
- * Analytics Module
2
+ * Analytics — MOVED to `@djangocfg/analytics`.
3
3
  *
4
- * Google Analytics integration with react-ga4
4
+ * This is a back-compat re-export so existing imports keep working. New code
5
+ * should import from `@djangocfg/analytics` directly.
6
+ *
7
+ * Why it moved: this module used to own its own route listener. Adding a second
8
+ * destination (the self-hosted first-party backend) would have meant a SECOND
9
+ * listener, double-firing pageviews with a drifting event taxonomy. The new
10
+ * package has one listener that fans out to N transports — GA4 is now just one
11
+ * of them, alongside the django-cfg ingest endpoint.
5
12
  */
6
13
 
7
- export { useAnalytics, Analytics } from './useAnalytics';
14
+ export { AnalyticsCategory, AnalyticsEvent, useAnalytics } from '@djangocfg/analytics';
15
+
16
+ export type { AnalyticsCategoryType, AnalyticsEventType } from '@djangocfg/analytics';
17
+
8
18
  export { AnalyticsProvider } from './AnalyticsProvider';
9
- export { AnalyticsEvent, AnalyticsCategory } from './events';
10
- export type { AnalyticsEventType, AnalyticsCategoryType } from './events';
11
19
  export type { AnalyticsConfig } from './types';
@@ -1,60 +0,0 @@
1
- /**
2
- * Analytics Events Constants
3
- *
4
- * Predefined event names and categories for consistent tracking
5
- * across the entire application.
6
- */
7
-
8
- /**
9
- * Event Categories
10
- */
11
- export const AnalyticsCategory = {
12
- AUTH: 'auth',
13
- ERROR: 'error',
14
- NAVIGATION: 'navigation',
15
- ENGAGEMENT: 'engagement',
16
- USER: 'user',
17
- } as const;
18
-
19
- /**
20
- * Predefined Event Names
21
- */
22
- export const AnalyticsEvent = {
23
- // Auth Events
24
- AUTH_OTP_REQUEST: 'auth_otp_request',
25
- AUTH_OTP_VERIFY_SUCCESS: 'auth_otp_verify_success',
26
- AUTH_OTP_VERIFY_FAIL: 'auth_otp_verify_fail',
27
- AUTH_LOGIN_SUCCESS: 'auth_login_success',
28
- AUTH_LOGOUT: 'auth_logout',
29
- AUTH_SESSION_EXPIRED: 'auth_session_expired',
30
- AUTH_TOKEN_REFRESH: 'auth_token_refresh',
31
- AUTH_TOKEN_REFRESH_FAIL: 'auth_token_refresh_fail',
32
-
33
- // OAuth Events
34
- AUTH_OAUTH_START: 'auth_oauth_start',
35
- AUTH_OAUTH_SUCCESS: 'auth_oauth_success',
36
- AUTH_OAUTH_FAIL: 'auth_oauth_fail',
37
-
38
- // Error Events
39
- ERROR_BOUNDARY: 'error_boundary',
40
- ERROR_API: 'error_api',
41
- ERROR_VALIDATION: 'error_validation',
42
- ERROR_NETWORK: 'error_network',
43
-
44
- // Navigation Events
45
- NAV_ADMIN_ENTER: 'nav_admin_enter',
46
- NAV_DASHBOARD_ENTER: 'nav_dashboard_enter',
47
- NAV_PAGE_VIEW: 'nav_page_view',
48
-
49
- // Engagement Events
50
- THEME_CHANGE: 'theme_change',
51
- SIDEBAR_TOGGLE: 'sidebar_toggle',
52
- MOBILE_MENU_OPEN: 'mobile_menu_open',
53
-
54
- // User Events
55
- USER_PROFILE_VIEW: 'user_profile_view',
56
- USER_PROFILE_UPDATE: 'user_profile_update',
57
- } as const;
58
-
59
- export type AnalyticsCategoryType = typeof AnalyticsCategory[keyof typeof AnalyticsCategory];
60
- export type AnalyticsEventType = typeof AnalyticsEvent[keyof typeof AnalyticsEvent];
@@ -1,142 +0,0 @@
1
- /**
2
- * useAnalytics Hook
3
- *
4
- * Provides Google Analytics tracking via react-ga4
5
- * Automatically tracks page views on route changes
6
- * Only works in production mode
7
- */
8
-
9
- 'use client';
10
-
11
- import { usePathname } from 'next/navigation';
12
- import { useEffect } from 'react';
13
- import ReactGA from 'react-ga4';
14
-
15
- import { useAuth } from '@djangocfg/api/auth';
16
-
17
- // Check if we're in production
18
- import { isProd as isProduction } from '@djangocfg/ui-core/lib';
19
-
20
- // Tracking state
21
- let isInitialized = false;
22
- // @ts-ignore reserved for future use (e.g., analytics debugging)
23
- let _currentTrackingId: string | undefined;
24
-
25
- /**
26
- * Analytics utility object for standalone usage (outside React components)
27
- *
28
- * @example
29
- * ```ts
30
- * import { Analytics } from '@djangocfg/layouts';
31
- *
32
- * // In an event handler or utility function
33
- * Analytics.event('button_click', { category: 'engagement', label: 'signup' });
34
- * ```
35
- */
36
- export const Analytics = {
37
- /**
38
- * Initialize Google Analytics (called automatically by useAnalytics hook)
39
- */
40
- init: (trackingId: string) => {
41
- if (!isProduction || !trackingId || isInitialized) return;
42
- ReactGA.initialize(trackingId);
43
- isInitialized = true;
44
- _currentTrackingId = trackingId;
45
- },
46
-
47
- /**
48
- * Check if Analytics is enabled and initialized
49
- */
50
- isEnabled: () => isProduction && isInitialized,
51
-
52
- /**
53
- * Track a page view
54
- */
55
- pageview: (path: string) => {
56
- if (!Analytics.isEnabled()) return;
57
- ReactGA.send({ hitType: 'pageview', page: path });
58
- },
59
-
60
- /**
61
- * Track a custom event
62
- * @param name - Event name (action)
63
- * @param params - Optional event parameters
64
- */
65
- event: (name: string, params: Record<string, any> = {}) => {
66
- if (!Analytics.isEnabled()) return;
67
- ReactGA.event(name, params);
68
- },
69
-
70
- /**
71
- * Set user ID for tracking
72
- */
73
- setUser: (userId: string) => {
74
- if (!Analytics.isEnabled()) return;
75
- ReactGA.set({ user_id: userId });
76
- },
77
-
78
- /**
79
- * Set custom dimensions/metrics
80
- */
81
- set: (fieldsObject: Record<string, any>) => {
82
- if (!Analytics.isEnabled()) return;
83
- ReactGA.set(fieldsObject);
84
- },
85
- };
86
-
87
- /**
88
- * Hook for Google Analytics tracking via react-ga4
89
- *
90
- * Automatically initializes GA and tracks page views on route changes
91
- * Only works in production mode (NODE_ENV === 'production')
92
- *
93
- * @example
94
- * ```tsx
95
- * // Just call the hook - it auto-tracks pageviews
96
- * useAnalytics();
97
- *
98
- * // Or use the returned methods for custom tracking
99
- * const { event, isEnabled } = useAnalytics();
100
- * event('button_click', { category: 'engagement', label: 'signup' });
101
- * ```
102
- */
103
- export function useAnalytics(trackingIdProp?: string) {
104
- const pathname = usePathname();
105
- const { user, isAuthenticated } = useAuth();
106
-
107
- // Use trackingId from prop (passed from AppLayout)
108
- const trackingId = trackingIdProp;
109
- const isEnabled = isProduction && Boolean(trackingId);
110
-
111
- // Initialize GA4
112
- useEffect(() => {
113
- if (!isEnabled || !trackingId) return;
114
- Analytics.init(trackingId);
115
- }, [isEnabled, trackingId]);
116
-
117
- // Auto-set user ID when authenticated
118
- useEffect(() => {
119
- if (!isEnabled || !isAuthenticated || !user?.id) return;
120
- Analytics.setUser(String(user.id));
121
- }, [isEnabled, isAuthenticated, user?.id]);
122
-
123
- // Auto-track page views on route change (App Router)
124
- useEffect(() => {
125
- if (!isEnabled || typeof window === 'undefined') return;
126
-
127
- // Track page view when pathname changes
128
- const url = pathname + (window.location.search || '');
129
- Analytics.pageview(url);
130
- }, [pathname, isEnabled]);
131
-
132
- return {
133
- isEnabled,
134
- trackingId,
135
- pageview: Analytics.pageview,
136
- event: Analytics.event,
137
- setUser: Analytics.setUser,
138
- set: Analytics.set,
139
- };
140
- }
141
-
142
- export default useAnalytics;