Files
donetick/src/analytics/consent.js
Mo Tarbin a58888f0e6 feat(analytics): integrate PostHog for event tracking and user consent management
- Added PostHog SDK to package.json for analytics tracking.
- Implemented analytics consent management with separate toggles for analytics and crash reporting.
- Created analytics module to handle initialization, event tracking, and user identification.
- Integrated analytics tracking into various components including onboarding, feedback prompts, and chore creation.
- Added PrivacyAnalyticsSettings view for managing user consent preferences.
- Enhanced feedback submission with analytics tracking for user interactions.
- Updated device information utility for better context in analytics events.
- Refactored existing code to utilize new analytics functions and ensure proper event sanitization.
2026-08-11 21:42:51 -04:00

108 lines
3.3 KiB
JavaScript

import { Preferences } from '@capacitor/preferences'
// Two independent consent axes, matching the existing onboarding UI
// (HeardAboutView's PrivacyPreferences): "analytics" gates track(), "crash"
// gates captureError(). A self-hosted user can opt into one without the other.
const CONSENT_KEYS = {
analytics: 'analytics_consent',
crash: 'analytics_crash_consent',
}
const ANON_ID_KEY = 'analytics_anon_id'
const INSTALLATION_ID_KEY = 'analytics_installation_id'
const generateUUID = () => {
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID()
}
// Fallback for webviews without crypto.randomUUID — not cryptographically
// strong, but this identifier carries no user information either way.
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = (Math.random() * 16) | 0
const v = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}
const readPreference = async key => {
try {
const { value } = await Preferences.get({ key })
return value ?? null
} catch {
return null
}
}
const writePreference = async (key, value) => {
try {
await Preferences.set({ key, value })
} catch {
// best-effort; consent falls back to 'unknown' on next read
}
}
const removePreference = async key => {
try {
await Preferences.remove({ key })
} catch {
// ignore
}
}
export const getStoredConsent = async kind => {
const value = await readPreference(CONSENT_KEYS[kind])
return value === 'enabled' || value === 'disabled' ? value : 'unknown'
}
export const setStoredConsent = (kind, value) =>
writePreference(CONSENT_KEYS[kind], value)
/**
* Self-hosted `unknown` behaves as disabled (opt-in required); cloud
* `unknown` behaves as enabled (opt-out available). Nothing is persisted by
* this resolution alone — only an explicit setConsent() call writes a value.
*/
export const resolveEffectiveConsent = (stored, deploymentType) => {
if (stored === 'enabled' || stored === 'disabled') return stored
return deploymentType === 'cloud' ? 'enabled' : 'disabled'
}
export const getOrCreateAnonId = async () => {
const existing = await readPreference(ANON_ID_KEY)
if (existing) return existing
const created = generateUUID()
await writePreference(ANON_ID_KEY, created)
return created
}
export const getOrCreateInstallationId = async () => {
const existing = await readPreference(INSTALLATION_ID_KEY)
if (existing) return existing
const created = generateUUID()
await writePreference(INSTALLATION_ID_KEY, created)
return created
}
/**
* Cloud + known user -> identify by the Donetick user id. Self-hosted (or
* cloud pre-login) -> a random id containing no user information, never
* derived from email/username/database id.
*/
export const resolveIdentity = async ({ deploymentType, userId }) => {
if (deploymentType === 'cloud' && userId) {
return { distinctId: String(userId), installationId: null }
}
const [anonId, installationId] = await Promise.all([
getOrCreateAnonId(),
deploymentType === 'cloud' ? null : getOrCreateInstallationId(),
])
return { distinctId: anonId, installationId }
}
export const clearAnonymousIdentity = async () => {
await Promise.all([
removePreference(ANON_ID_KEY),
removePreference(INSTALLATION_ID_KEY),
])
}