Release 0.15.0: Amplitude Analytics + Session Replay (with #21 Part C identity lifecycle)
Wave 5 / Track A. Roadmap item #13. Folds in #21 Part C user-identity- lifecycle work inline rather than as a follow-up release (operator ask: "implement Amplitude best practices from the very get-go"). Wraps @amplitude/unified with: - consent-gated lazy init (v0.13.0 cookie banner; SDK never loads without explicit analytics opt-in) - amplitude.initAll(KEY, { analytics: { autocapture: true }, sessionReplay: { sampleRate: 1 } }) — vendor-recommended shape - identify({ user_id, properties? }) with set vs setOnce semantics - setUserProperties(properties) for mid-session state changes - anonymize() clears both user_id binding AND property cache Wired into App.jsx (identify on me.user with role / permission_state / passcode_set / device_trusted as set; first_sign_in_at / account_created_at as setOnce; Page Viewed on route change; Sign-out fires User Signed Out + anonymize), Login.jsx (User Signed In + Beta Access Requested), and the per-feature surfaces (ProposeModal, PRModal, RFCView, RFCDiscussionPanel, PRView, Admin). Binding: VITE_AMPLITUDE_API_KEY via `flotilla overlay set` (bundle-embedded public key like VITE_TURNSTILE_SITE_KEY — not secret). Mid-Session-L correction: original dispatch brief specified secret-set; vendor guidance + bundle visibility settled it as overlay. PII discipline: no email, no display_name, no gitea_login, no free-text fields ever sent. Properties are opaque ids + enums + timestamps + booleans only. Subagent ξ shipped the wrapper structure + nine-event taxonomy on feature/v0.15.0-amplitude (0fd8c52+6cfbf69post-correction). Driver-side integration extended the wrapper with setUserProperties + identify properties for the #21 Part C identity-lifecycle work. Frontend build verified green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -85,6 +85,7 @@ import {
|
||||
startDeviceTrust,
|
||||
} from '../api'
|
||||
import TurnstileWidget, { turnstileEnabled } from './TurnstileWidget'
|
||||
import { EVENTS, track } from '../lib/analytics'
|
||||
|
||||
export default function Login() {
|
||||
// Steps: 'email' → 'passcode' or 'code' → (on the OTC path, after
|
||||
@@ -150,7 +151,12 @@ export default function Login() {
|
||||
;(async () => {
|
||||
try {
|
||||
await startDeviceTrust()
|
||||
if (!cancelled) window.location.assign('/')
|
||||
if (!cancelled) {
|
||||
// v0.15.0 — analytics: device-trust cookie path is one of
|
||||
// three sign-in methods the taxonomy distinguishes.
|
||||
track(EVENTS.USER_SIGNED_IN, { method: 'trust-device' })
|
||||
window.location.assign('/')
|
||||
}
|
||||
} catch (_) {
|
||||
// No trusted device — fall through to the email step.
|
||||
}
|
||||
@@ -206,6 +212,10 @@ export default function Login() {
|
||||
setStatus('')
|
||||
try {
|
||||
await verifyPasscode(email.trim(), passcode.trim(), { trustDevice })
|
||||
// v0.15.0 — analytics: passcode is the second of three
|
||||
// sign-in methods. trust-device gets credited separately when
|
||||
// the cookie-driven path fires above.
|
||||
track(EVENTS.USER_SIGNED_IN, { method: 'passcode' })
|
||||
// Reload so App.jsx's getMe() picks up the fresh session. A
|
||||
// returning passcode user is by definition already past the
|
||||
// §6.1 capture step (they couldn't have set a passcode while
|
||||
@@ -264,6 +274,11 @@ export default function Login() {
|
||||
setStatus('')
|
||||
try {
|
||||
await verifyOtc(email.trim(), code.trim(), { trustDevice })
|
||||
// v0.15.0 — analytics: OTC is the third sign-in method.
|
||||
// We fire it here regardless of whether the user then lands
|
||||
// in capture-profile or offer-passcode — sign-in has happened
|
||||
// server-side either way.
|
||||
track(EVENTS.USER_SIGNED_IN, { method: 'otc' })
|
||||
// OTC verified — the server has signed in the user. Fetch the
|
||||
// canonical /api/auth/me to decide where to land:
|
||||
// * needs_profile → §6.1 capture (then /beta-pending).
|
||||
@@ -320,6 +335,11 @@ export default function Login() {
|
||||
last_name: ln,
|
||||
beta_request_reason: why,
|
||||
})
|
||||
// v0.15.0 — analytics: a successful capture-profile submit is
|
||||
// the moment a beta-access request lands. No PII in the event
|
||||
// body (no name, no reason text); the count + timestamp is
|
||||
// what the funnel needs.
|
||||
track(EVENTS.BETA_ACCESS_REQUESTED)
|
||||
// Hard-load so App.jsx re-fetches /api/auth/me and picks up
|
||||
// the captured fields. The user stays permission_state='pending'
|
||||
// until an admin grants access — the next thing they should
|
||||
|
||||
Reference in New Issue
Block a user