@mars-stack/cli 0.2.0 → 1.0.2
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/dist/index.js +137 -12
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/template/.cursor/rules/composition-patterns.mdc +186 -0
- package/template/.cursor/rules/data-access.mdc +29 -0
- package/template/.cursor/rules/project-structure.mdc +34 -0
- package/template/.cursor/rules/security.mdc +25 -0
- package/template/.cursor/rules/testing.mdc +24 -0
- package/template/.cursor/rules/ui-conventions.mdc +29 -0
- package/template/.cursor/skills/add-api-route/SKILL.md +122 -0
- package/template/.cursor/skills/add-audit-log/SKILL.md +375 -0
- package/template/.cursor/skills/add-blog/SKILL.md +447 -0
- package/template/.cursor/skills/add-command-palette/SKILL.md +438 -0
- package/template/.cursor/skills/add-component/SKILL.md +158 -0
- package/template/.cursor/skills/add-crud-routes/SKILL.md +221 -0
- package/template/.cursor/skills/add-e2e-test/SKILL.md +227 -0
- package/template/.cursor/skills/add-error-boundary/SKILL.md +472 -0
- package/template/.cursor/skills/add-feature/SKILL.md +174 -0
- package/template/.cursor/skills/add-middleware/SKILL.md +135 -0
- package/template/.cursor/skills/add-page/SKILL.md +151 -0
- package/template/.cursor/skills/add-prisma-model/SKILL.md +148 -0
- package/template/.cursor/skills/add-protected-resource/SKILL.md +192 -0
- package/template/.cursor/skills/add-role/SKILL.md +156 -0
- package/template/.cursor/skills/add-server-action/SKILL.md +167 -0
- package/template/.cursor/skills/add-webhook/SKILL.md +192 -0
- package/template/.cursor/skills/build-complete-feature/SKILL.md +227 -0
- package/template/.cursor/skills/build-dashboard/SKILL.md +211 -0
- package/template/.cursor/skills/build-data-table/SKILL.md +283 -0
- package/template/.cursor/skills/build-form/SKILL.md +231 -0
- package/template/.cursor/skills/build-landing-page/SKILL.md +248 -0
- package/template/.cursor/skills/configure-ai/SKILL.md +617 -0
- package/template/.cursor/skills/configure-analytics/SKILL.md +413 -0
- package/template/.cursor/skills/configure-dark-mode/SKILL.md +309 -0
- package/template/.cursor/skills/configure-email/SKILL.md +170 -0
- package/template/.cursor/skills/configure-email-verification/SKILL.md +333 -0
- package/template/.cursor/skills/configure-feature-flags/SKILL.md +361 -0
- package/template/.cursor/skills/configure-i18n/SKILL.md +518 -0
- package/template/.cursor/skills/configure-jobs/SKILL.md +500 -0
- package/template/.cursor/skills/configure-magic-links/SKILL.md +385 -0
- package/template/.cursor/skills/configure-multi-tenancy/SKILL.md +611 -0
- package/template/.cursor/skills/configure-notifications/SKILL.md +569 -0
- package/template/.cursor/skills/configure-oauth/SKILL.md +217 -0
- package/template/.cursor/skills/configure-onboarding/SKILL.md +483 -0
- package/template/.cursor/skills/configure-payments/SKILL.md +243 -0
- package/template/.cursor/skills/configure-realtime/SKILL.md +733 -0
- package/template/.cursor/skills/configure-search/SKILL.md +581 -0
- package/template/.cursor/skills/configure-storage/SKILL.md +273 -0
- package/template/.cursor/skills/configure-two-factor/SKILL.md +518 -0
- package/template/.cursor/skills/create-execution-plan/SKILL.md +204 -0
- package/template/.cursor/skills/create-seed/SKILL.md +191 -0
- package/template/.cursor/skills/deploy-to-vercel/SKILL.md +300 -0
- package/template/.cursor/skills/design-tokens/SKILL.md +138 -0
- package/template/.cursor/skills/mars-capture-conversation-context/SKILL.md +119 -0
- package/template/.cursor/skills/setup-billing/SKILL.md +322 -0
- package/template/.cursor/skills/setup-project/SKILL.md +104 -0
- package/template/.cursor/skills/setup-teams/SKILL.md +682 -0
- package/template/.cursor/skills/test-api-route/SKILL.md +219 -0
- package/template/.cursor/skills/update-architecture-docs/SKILL.md +99 -0
- package/template/AGENTS.md +104 -0
- package/template/ARCHITECTURE.md +102 -0
- package/template/docs/QUALITY_SCORE.md +20 -0
- package/template/docs/design-docs/conversation-as-system-record.md +70 -0
- package/template/docs/design-docs/core-beliefs.md +43 -0
- package/template/docs/design-docs/index.md +8 -0
- package/template/docs/exec-plans/active/.gitkeep +0 -0
- package/template/docs/exec-plans/completed/.gitkeep +0 -0
- package/template/docs/exec-plans/tech-debt.md +7 -0
- package/template/docs/generated/.gitkeep +0 -0
- package/template/docs/product-specs/index.md +7 -0
- package/template/docs/references/index.md +18 -0
- package/template/e2e/api.spec.ts +20 -0
- package/template/e2e/auth.spec.ts +24 -0
- package/template/e2e/public.spec.ts +25 -0
- package/template/eslint.config.mjs +24 -0
- package/template/next-env.d.ts +6 -0
- package/template/next.config.ts +45 -0
- package/template/package.json +80 -0
- package/template/playwright.config.ts +31 -0
- package/template/postcss.config.mjs +8 -0
- package/template/prisma/generated/prisma/browser.ts +49 -0
- package/template/prisma/generated/prisma/client.ts +73 -0
- package/template/prisma/generated/prisma/commonInputTypes.ts +406 -0
- package/template/prisma/generated/prisma/enums.ts +15 -0
- package/template/prisma/generated/prisma/internal/class.ts +254 -0
- package/template/prisma/generated/prisma/internal/prismaNamespace.ts +1240 -0
- package/template/prisma/generated/prisma/internal/prismaNamespaceBrowser.ts +190 -0
- package/template/prisma/generated/prisma/models/Account.ts +1543 -0
- package/template/prisma/generated/prisma/models/File.ts +1529 -0
- package/template/prisma/generated/prisma/models/Session.ts +1415 -0
- package/template/prisma/generated/prisma/models/Subscription.ts +1455 -0
- package/template/prisma/generated/prisma/models/User.ts +2235 -0
- package/template/prisma/generated/prisma/models/VerificationToken.ts +1099 -0
- package/template/prisma/generated/prisma/models.ts +17 -0
- package/template/prisma/schema/auth.prisma +69 -0
- package/template/prisma/schema/base.prisma +8 -0
- package/template/prisma/schema/file.prisma +15 -0
- package/template/prisma/schema/subscription.prisma +17 -0
- package/template/prisma.config.ts +13 -0
- package/template/scripts/check-architecture.ts +221 -0
- package/template/scripts/check-doc-freshness.ts +242 -0
- package/template/scripts/ensure-db.mjs +291 -0
- package/template/scripts/generate-docs.ts +143 -0
- package/template/scripts/generate-env-example.ts +89 -0
- package/template/scripts/seed.ts +56 -0
- package/template/scripts/update-quality-score.ts +263 -0
- package/template/src/__tests__/architecture.test.ts +114 -0
- package/template/src/app/(auth)/forgotten-password/page.tsx +92 -0
- package/template/src/app/(auth)/layout.tsx +11 -0
- package/template/src/app/(auth)/register/page.tsx +162 -0
- package/template/src/app/(auth)/reset-password/page.tsx +109 -0
- package/template/src/app/(auth)/sign-in/page.tsx +122 -0
- package/template/src/app/(auth)/verify/[token]/page.tsx +87 -0
- package/template/src/app/(auth)/verify/page.tsx +56 -0
- package/template/src/app/(protected)/admin/page.tsx +108 -0
- package/template/src/app/(protected)/dashboard/loading.tsx +20 -0
- package/template/src/app/(protected)/dashboard/page.tsx +22 -0
- package/template/src/app/(protected)/layout.tsx +262 -0
- package/template/src/app/(protected)/settings/page.tsx +370 -0
- package/template/src/app/api/auth/forgot/route.ts +63 -0
- package/template/src/app/api/auth/login/route.ts +121 -0
- package/template/src/app/api/auth/logout/route.ts +19 -0
- package/template/src/app/api/auth/me/route.ts +30 -0
- package/template/src/app/api/auth/reset/route.ts +45 -0
- package/template/src/app/api/auth/signup/route.ts +85 -0
- package/template/src/app/api/auth/verify/route.ts +46 -0
- package/template/src/app/api/csrf/route.ts +12 -0
- package/template/src/app/api/health/route.ts +10 -0
- package/template/src/app/api/protected/admin/users/route.ts +24 -0
- package/template/src/app/api/protected/billing/checkout/route.ts +83 -0
- package/template/src/app/api/protected/billing/portal/route.ts +39 -0
- package/template/src/app/api/protected/files/[fileId]/route.ts +86 -0
- package/template/src/app/api/protected/files/upload/route.ts +64 -0
- package/template/src/app/api/protected/user/password/route.ts +63 -0
- package/template/src/app/api/protected/user/profile/route.ts +35 -0
- package/template/src/app/api/protected/user/sessions/[sessionId]/route.ts +33 -0
- package/template/src/app/api/protected/user/sessions/route.ts +22 -0
- package/template/src/app/api/readiness/route.ts +15 -0
- package/template/src/app/api/webhooks/stripe/route.ts +166 -0
- package/template/src/app/error.tsx +33 -0
- package/template/src/app/layout.tsx +29 -0
- package/template/src/app/not-found.tsx +20 -0
- package/template/src/app/page.tsx +136 -0
- package/template/src/app/privacy/page.tsx +178 -0
- package/template/src/app/providers.tsx +8 -0
- package/template/src/app/terms/page.tsx +139 -0
- package/template/src/config/app.config.ts +70 -0
- package/template/src/config/routes.ts +17 -0
- package/template/src/features/admin/index.ts +11 -0
- package/template/src/features/admin/permissions.ts +64 -0
- package/template/src/features/auth/context/AuthContext.tsx +96 -0
- package/template/src/features/auth/context/index.ts +2 -0
- package/template/src/features/auth/index.ts +3 -0
- package/template/src/features/auth/server/consent.ts +66 -0
- package/template/src/features/auth/server/session-revocation.ts +20 -0
- package/template/src/features/auth/server/sessions.ts +66 -0
- package/template/src/features/auth/server/user.ts +166 -0
- package/template/src/features/auth/types.ts +19 -0
- package/template/src/features/auth/validators.ts +29 -0
- package/template/src/features/billing/server/index.ts +66 -0
- package/template/src/features/billing/types.ts +43 -0
- package/template/src/features/uploads/server/index.ts +49 -0
- package/template/src/features/uploads/types.ts +26 -0
- package/template/src/lib/core/email/templates/base-layout.ts +122 -0
- package/template/src/lib/core/email/templates/index.ts +4 -0
- package/template/src/lib/core/email/templates/password-reset-email.ts +42 -0
- package/template/src/lib/core/email/templates/verification-email.ts +41 -0
- package/template/src/lib/core/email/templates/welcome-email.ts +40 -0
- package/template/src/lib/mars.ts +56 -0
- package/template/src/lib/prisma.ts +19 -0
- package/template/src/proxy.ts +92 -0
- package/template/src/styles/brand.css +15 -0
- package/template/src/styles/globals.css +7 -0
- package/template/tsconfig.json +59 -0
- package/template/vitest.config.ts +41 -0
- package/template/vitest.setup.ts +24 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
# Skill: Configure Analytics
|
|
2
|
+
|
|
3
|
+
Set up analytics tracking with Vercel Analytics, PostHog, or Google Analytics in a MARS application.
|
|
4
|
+
|
|
5
|
+
## When to Use
|
|
6
|
+
|
|
7
|
+
Use this skill when the user asks to add analytics, tracking, page views, event tracking, Vercel Analytics, PostHog, Google Analytics, or a consent banner.
|
|
8
|
+
|
|
9
|
+
## Prerequisites
|
|
10
|
+
|
|
11
|
+
- Next.js app set up with the MARS template
|
|
12
|
+
|
|
13
|
+
## Architecture
|
|
14
|
+
|
|
15
|
+
MARS supports multiple analytics providers through a unified wrapper. The active provider is set in `appConfig.services.analytics`. All tracking calls go through a single `trackEvent` function, making it easy to swap providers.
|
|
16
|
+
|
|
17
|
+
Supported providers:
|
|
18
|
+
- `vercel` — Vercel Analytics + Web Vitals (zero-config for Vercel deployments)
|
|
19
|
+
- `posthog` — PostHog (self-hostable, feature flags, session replay)
|
|
20
|
+
- `google` — Google Analytics 4 (gtag.js)
|
|
21
|
+
- `none` — disabled (default)
|
|
22
|
+
|
|
23
|
+
## Step 1: Configuration
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
// src/config/app.config.ts
|
|
27
|
+
services: {
|
|
28
|
+
analytics: {
|
|
29
|
+
provider: 'none' as 'vercel' | 'posthog' | 'google' | 'none',
|
|
30
|
+
enableInDevelopment: false,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Step 2: Install Provider SDK
|
|
36
|
+
|
|
37
|
+
**Vercel Analytics:**
|
|
38
|
+
```bash
|
|
39
|
+
yarn add @vercel/analytics @vercel/speed-insights
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**PostHog:**
|
|
43
|
+
```bash
|
|
44
|
+
yarn add posthog-js
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Google Analytics:**
|
|
48
|
+
No package needed — uses the gtag.js script tag.
|
|
49
|
+
|
|
50
|
+
## Step 3: Environment Variables
|
|
51
|
+
|
|
52
|
+
**Vercel Analytics:**
|
|
53
|
+
No env vars needed — auto-configured on Vercel deployments.
|
|
54
|
+
|
|
55
|
+
**PostHog:**
|
|
56
|
+
```bash
|
|
57
|
+
NEXT_PUBLIC_POSTHOG_KEY="phc_your-project-key"
|
|
58
|
+
NEXT_PUBLIC_POSTHOG_HOST="https://us.i.posthog.com"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Google Analytics:**
|
|
62
|
+
```bash
|
|
63
|
+
NEXT_PUBLIC_GA_MEASUREMENT_ID="G-XXXXXXXXXX"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Step 4: Analytics Provider Component
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// src/lib/shared/components/providers/AnalyticsProvider.tsx
|
|
70
|
+
'use client';
|
|
71
|
+
|
|
72
|
+
import { useEffect } from 'react';
|
|
73
|
+
import { usePathname, useSearchParams } from 'next/navigation';
|
|
74
|
+
import { appConfig } from '@/config/app.config';
|
|
75
|
+
|
|
76
|
+
function usePageView() {
|
|
77
|
+
const pathname = usePathname();
|
|
78
|
+
const searchParams = useSearchParams();
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!pathname) return;
|
|
82
|
+
|
|
83
|
+
const url = searchParams?.toString()
|
|
84
|
+
? `${pathname}?${searchParams.toString()}`
|
|
85
|
+
: pathname;
|
|
86
|
+
|
|
87
|
+
trackPageView(url);
|
|
88
|
+
}, [pathname, searchParams]);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function AnalyticsProvider({ children }: { children: React.ReactNode }) {
|
|
92
|
+
const provider = appConfig.services.analytics.provider;
|
|
93
|
+
const isDev = process.env.NODE_ENV === 'development';
|
|
94
|
+
|
|
95
|
+
if (provider === 'none' || (isDev && !appConfig.services.analytics.enableInDevelopment)) {
|
|
96
|
+
return <>{children}</>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<>
|
|
101
|
+
{provider === 'vercel' && <VercelAnalytics />}
|
|
102
|
+
{provider === 'posthog' && <PostHogProvider />}
|
|
103
|
+
{provider === 'google' && <GoogleAnalytics />}
|
|
104
|
+
<PageViewTracker />
|
|
105
|
+
{children}
|
|
106
|
+
</>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function PageViewTracker() {
|
|
111
|
+
usePageView();
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Step 5: Vercel Analytics Setup
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
// src/lib/shared/components/providers/analytics/VercelAnalytics.tsx
|
|
120
|
+
'use client';
|
|
121
|
+
|
|
122
|
+
import { Analytics } from '@vercel/analytics/react';
|
|
123
|
+
import { SpeedInsights } from '@vercel/speed-insights/next';
|
|
124
|
+
|
|
125
|
+
export function VercelAnalytics() {
|
|
126
|
+
return (
|
|
127
|
+
<>
|
|
128
|
+
<Analytics />
|
|
129
|
+
<SpeedInsights />
|
|
130
|
+
</>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Step 6: PostHog Setup
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// src/lib/shared/components/providers/analytics/PostHogProvider.tsx
|
|
139
|
+
'use client';
|
|
140
|
+
|
|
141
|
+
import posthog from 'posthog-js';
|
|
142
|
+
import { PostHogProvider as PHProvider } from 'posthog-js/react';
|
|
143
|
+
import { useEffect, useState } from 'react';
|
|
144
|
+
|
|
145
|
+
export function PostHogProvider({ children }: { children?: React.ReactNode }) {
|
|
146
|
+
const [initialized, setInitialized] = useState(false);
|
|
147
|
+
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
const key = process.env.NEXT_PUBLIC_POSTHOG_KEY;
|
|
150
|
+
const host = process.env.NEXT_PUBLIC_POSTHOG_HOST;
|
|
151
|
+
|
|
152
|
+
if (!key) {
|
|
153
|
+
console.warn('PostHog: NEXT_PUBLIC_POSTHOG_KEY is not set');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
posthog.init(key, {
|
|
158
|
+
api_host: host || 'https://us.i.posthog.com',
|
|
159
|
+
person_profiles: 'identified_only',
|
|
160
|
+
capture_pageview: false, // We handle this manually for SPA navigation
|
|
161
|
+
capture_pageleave: true,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
setInitialized(true);
|
|
165
|
+
}, []);
|
|
166
|
+
|
|
167
|
+
if (!initialized) return <>{children}</>;
|
|
168
|
+
|
|
169
|
+
return <PHProvider client={posthog}>{children}</PHProvider>;
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Step 7: Google Analytics Setup
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
// src/lib/shared/components/providers/analytics/GoogleAnalytics.tsx
|
|
177
|
+
'use client';
|
|
178
|
+
|
|
179
|
+
import Script from 'next/script';
|
|
180
|
+
|
|
181
|
+
export function GoogleAnalytics() {
|
|
182
|
+
const measurementId = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID;
|
|
183
|
+
|
|
184
|
+
if (!measurementId) {
|
|
185
|
+
console.warn('Google Analytics: NEXT_PUBLIC_GA_MEASUREMENT_ID is not set');
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<>
|
|
191
|
+
<Script
|
|
192
|
+
strategy="afterInteractive"
|
|
193
|
+
src={`https://www.googletagmanager.com/gtag/js?id=${measurementId}`}
|
|
194
|
+
/>
|
|
195
|
+
<Script
|
|
196
|
+
id="google-analytics"
|
|
197
|
+
strategy="afterInteractive"
|
|
198
|
+
dangerouslySetInnerHTML={{
|
|
199
|
+
__html: `
|
|
200
|
+
window.dataLayer = window.dataLayer || [];
|
|
201
|
+
function gtag(){dataLayer.push(arguments);}
|
|
202
|
+
gtag('js', new Date());
|
|
203
|
+
gtag('config', '${measurementId}', {
|
|
204
|
+
page_path: window.location.pathname,
|
|
205
|
+
});
|
|
206
|
+
`,
|
|
207
|
+
}}
|
|
208
|
+
/>
|
|
209
|
+
</>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Step 8: Unified Tracking API
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
// src/lib/shared/utils/analytics.ts
|
|
218
|
+
import { appConfig } from '@/config/app.config';
|
|
219
|
+
|
|
220
|
+
export function trackPageView(url: string) {
|
|
221
|
+
const provider = appConfig.services.analytics.provider;
|
|
222
|
+
|
|
223
|
+
switch (provider) {
|
|
224
|
+
case 'posthog': {
|
|
225
|
+
const posthog = (window as any).posthog;
|
|
226
|
+
posthog?.capture('$pageview', { $current_url: url });
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
case 'google': {
|
|
230
|
+
const gtag = (window as any).gtag;
|
|
231
|
+
gtag?.('config', process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID, { page_path: url });
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
case 'vercel':
|
|
235
|
+
// Vercel Analytics handles page views automatically
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function trackEvent(
|
|
241
|
+
eventName: string,
|
|
242
|
+
properties?: Record<string, string | number | boolean>,
|
|
243
|
+
) {
|
|
244
|
+
const provider = appConfig.services.analytics.provider;
|
|
245
|
+
|
|
246
|
+
if (process.env.NODE_ENV === 'development' && !appConfig.services.analytics.enableInDevelopment) {
|
|
247
|
+
console.debug(`[analytics] ${eventName}`, properties);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
switch (provider) {
|
|
252
|
+
case 'posthog': {
|
|
253
|
+
const posthog = (window as any).posthog;
|
|
254
|
+
posthog?.capture(eventName, properties);
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
case 'google': {
|
|
258
|
+
const gtag = (window as any).gtag;
|
|
259
|
+
gtag?.('event', eventName, properties);
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
case 'vercel': {
|
|
263
|
+
// Vercel Analytics custom events via @vercel/analytics
|
|
264
|
+
import('@vercel/analytics').then(({ track }) => track(eventName, properties));
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function identifyUser(userId: string, traits?: Record<string, string>) {
|
|
271
|
+
const provider = appConfig.services.analytics.provider;
|
|
272
|
+
|
|
273
|
+
switch (provider) {
|
|
274
|
+
case 'posthog': {
|
|
275
|
+
const posthog = (window as any).posthog;
|
|
276
|
+
posthog?.identify(userId, traits);
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
case 'google': {
|
|
280
|
+
const gtag = (window as any).gtag;
|
|
281
|
+
gtag?.('set', { user_id: userId, ...traits });
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Step 9: Consent Banner
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
// src/lib/shared/components/patterns/ConsentBanner.tsx
|
|
292
|
+
'use client';
|
|
293
|
+
|
|
294
|
+
import { useEffect, useState } from 'react';
|
|
295
|
+
|
|
296
|
+
const CONSENT_KEY = 'mars-analytics-consent';
|
|
297
|
+
|
|
298
|
+
export function ConsentBanner() {
|
|
299
|
+
const [visible, setVisible] = useState(false);
|
|
300
|
+
|
|
301
|
+
useEffect(() => {
|
|
302
|
+
const consent = localStorage.getItem(CONSENT_KEY);
|
|
303
|
+
if (consent === null) setVisible(true);
|
|
304
|
+
}, []);
|
|
305
|
+
|
|
306
|
+
function handleAccept() {
|
|
307
|
+
localStorage.setItem(CONSENT_KEY, 'granted');
|
|
308
|
+
setVisible(false);
|
|
309
|
+
window.location.reload();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function handleDecline() {
|
|
313
|
+
localStorage.setItem(CONSENT_KEY, 'denied');
|
|
314
|
+
setVisible(false);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (!visible) return null;
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
<div className="fixed bottom-0 left-0 right-0 z-50 border-t border-border-primary bg-surface-primary p-4 shadow-lg">
|
|
321
|
+
<div className="mx-auto flex max-w-4xl items-center justify-between gap-4">
|
|
322
|
+
<p className="text-sm text-content-secondary">
|
|
323
|
+
We use cookies and analytics to improve your experience. You can opt out at any time.
|
|
324
|
+
</p>
|
|
325
|
+
<div className="flex shrink-0 gap-2">
|
|
326
|
+
<button
|
|
327
|
+
onClick={handleDecline}
|
|
328
|
+
className="rounded-md px-4 py-2 text-sm text-content-secondary hover:text-content-primary"
|
|
329
|
+
>
|
|
330
|
+
Decline
|
|
331
|
+
</button>
|
|
332
|
+
<button
|
|
333
|
+
onClick={handleAccept}
|
|
334
|
+
className="rounded-md bg-interactive-primary px-4 py-2 text-sm text-white hover:bg-interactive-primary-hover"
|
|
335
|
+
>
|
|
336
|
+
Accept
|
|
337
|
+
</button>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Update the `AnalyticsProvider` to check consent:
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
const consent = typeof window !== 'undefined' ? localStorage.getItem('mars-analytics-consent') : null;
|
|
349
|
+
if (consent === 'denied') return <>{children}</>;
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Step 10: Add to Root Layout
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
// src/app/layout.tsx
|
|
356
|
+
import { AnalyticsProvider } from '@/lib/shared/components/providers/AnalyticsProvider';
|
|
357
|
+
import { ConsentBanner } from '@/lib/shared/components/patterns/ConsentBanner';
|
|
358
|
+
|
|
359
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
360
|
+
return (
|
|
361
|
+
<html lang="en">
|
|
362
|
+
<body>
|
|
363
|
+
<AnalyticsProvider>
|
|
364
|
+
{children}
|
|
365
|
+
<ConsentBanner />
|
|
366
|
+
</AnalyticsProvider>
|
|
367
|
+
</body>
|
|
368
|
+
</html>
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## Usage Examples
|
|
374
|
+
|
|
375
|
+
Track a custom event anywhere in client code:
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
import { trackEvent } from '@/lib/shared/utils/analytics';
|
|
379
|
+
|
|
380
|
+
trackEvent('button_clicked', { button: 'upgrade', plan: 'pro' });
|
|
381
|
+
trackEvent('feature_used', { feature: 'export', format: 'csv' });
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Identify a user after login:
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
import { identifyUser } from '@/lib/shared/utils/analytics';
|
|
388
|
+
|
|
389
|
+
identifyUser(session.userId, { email: session.email, plan: 'free' });
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## Testing
|
|
393
|
+
|
|
394
|
+
1. Set provider to `'none'` — verify no scripts load and events log to console in dev.
|
|
395
|
+
2. Set provider to `'vercel'` — deploy to Vercel, check the Analytics dashboard.
|
|
396
|
+
3. Set provider to `'posthog'` — verify events appear in PostHog project.
|
|
397
|
+
4. Set provider to `'google'` — check Google Analytics Realtime view.
|
|
398
|
+
5. Decline consent — verify no tracking scripts load.
|
|
399
|
+
6. Accept consent — verify tracking resumes after reload.
|
|
400
|
+
7. Navigate between pages — verify page views are tracked.
|
|
401
|
+
|
|
402
|
+
## Checklist
|
|
403
|
+
|
|
404
|
+
- [ ] Analytics provider set in `appConfig.services.analytics.provider`
|
|
405
|
+
- [ ] Provider SDK installed (Vercel, PostHog, or gtag)
|
|
406
|
+
- [ ] Environment variables set for the chosen provider
|
|
407
|
+
- [ ] `AnalyticsProvider` wraps the app in root layout
|
|
408
|
+
- [ ] Unified `trackEvent`, `trackPageView`, and `identifyUser` functions
|
|
409
|
+
- [ ] Page view tracking on SPA navigation
|
|
410
|
+
- [ ] Consent banner with `localStorage` persistence
|
|
411
|
+
- [ ] Analytics disabled when consent is denied
|
|
412
|
+
- [ ] Development mode logging to console
|
|
413
|
+
- [ ] No tracking in development unless `enableInDevelopment` is set
|