@amplitude/wizard 1.0.0-beta.2 → 1.0.0-beta.6
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/LICENSE +25 -1
- package/README.md +171 -74
- package/dist/bin.js +338 -222
- package/dist/src/lib/agent-interface.js +64 -9
- package/dist/src/lib/agent-runner.js +1 -10
- package/dist/src/lib/api.d.ts +22 -4
- package/dist/src/lib/api.js +114 -12
- package/dist/src/lib/commandments.js +14 -1
- package/dist/src/lib/constants.d.ts +6 -5
- package/dist/src/lib/constants.js +13 -13
- package/dist/src/lib/credential-resolution.d.ts +45 -0
- package/dist/src/lib/credential-resolution.js +311 -0
- package/dist/src/lib/exit-codes.d.ts +10 -0
- package/dist/src/lib/exit-codes.js +12 -0
- package/dist/src/lib/health-checks/statuspage.d.ts +1 -0
- package/dist/src/lib/health-checks/statuspage.js +5 -1
- package/dist/src/lib/mode-config.d.ts +14 -0
- package/dist/src/lib/mode-config.js +14 -0
- package/dist/src/lib/session-checkpoint.d.ts +27 -0
- package/dist/src/lib/session-checkpoint.js +134 -0
- package/dist/src/lib/wizard-session.d.ts +44 -1
- package/dist/src/lib/wizard-session.js +70 -14
- package/dist/src/lib/wizard-tools.js +19 -4
- package/dist/src/steps/add-mcp-server-to-clients/clients/claude.d.ts +3 -0
- package/dist/src/steps/add-mcp-server-to-clients/clients/claude.js +6 -0
- package/dist/src/steps/add-mcp-server-to-clients/clients/cursor.js +3 -1
- package/dist/src/ui/agent-ui.d.ts +91 -0
- package/dist/src/ui/agent-ui.js +277 -0
- package/dist/src/ui/logging-ui.js +1 -1
- package/dist/src/ui/tui/App.d.ts +12 -0
- package/dist/src/ui/tui/App.js +29 -18
- package/dist/src/ui/tui/components/AmplitudeLogo.js +16 -17
- package/dist/src/ui/tui/components/AmplitudeTextLogo.d.ts +0 -2
- package/dist/src/ui/tui/components/AmplitudeTextLogo.js +53 -18
- package/dist/src/ui/tui/components/BrailleSpinner.d.ts +8 -0
- package/dist/src/ui/tui/components/BrailleSpinner.js +15 -0
- package/dist/src/ui/tui/components/ConsoleView.d.ts +8 -11
- package/dist/src/ui/tui/components/ConsoleView.js +51 -34
- package/dist/src/ui/tui/components/HeaderBar.d.ts +12 -0
- package/dist/src/ui/tui/components/HeaderBar.js +17 -0
- package/dist/src/ui/tui/components/JourneyStepper.d.ts +16 -0
- package/dist/src/ui/tui/components/JourneyStepper.js +83 -0
- package/dist/src/ui/tui/components/KeyHintBar.d.ts +19 -0
- package/dist/src/ui/tui/components/KeyHintBar.js +20 -0
- package/dist/src/ui/tui/console-commands.d.ts +1 -2
- package/dist/src/ui/tui/console-commands.js +48 -7
- package/dist/src/ui/tui/flows.d.ts +1 -1
- package/dist/src/ui/tui/flows.js +1 -1
- package/dist/src/ui/tui/hooks/useAsyncEffect.d.ts +15 -0
- package/dist/src/ui/tui/hooks/useAsyncEffect.js +35 -0
- package/dist/src/ui/tui/hooks/useWizardStore.d.ts +9 -0
- package/dist/src/ui/tui/hooks/useWizardStore.js +11 -0
- package/dist/src/ui/tui/ink-ui.js +1 -1
- package/dist/src/ui/tui/primitives/DissolveTransition.js +4 -5
- package/dist/src/ui/tui/primitives/EventPlanViewer.d.ts +3 -1
- package/dist/src/ui/tui/primitives/EventPlanViewer.js +8 -3
- package/dist/src/ui/tui/primitives/ProgressList.js +1 -1
- package/dist/src/ui/tui/primitives/SlashCommandInput.js +19 -4
- package/dist/src/ui/tui/primitives/SplitView.d.ts +2 -1
- package/dist/src/ui/tui/primitives/SplitView.js +10 -2
- package/dist/src/ui/tui/primitives/TabContainer.js +10 -2
- package/dist/src/ui/tui/primitives/index.d.ts +0 -1
- package/dist/src/ui/tui/primitives/index.js +0 -1
- package/dist/src/ui/tui/router.js +1 -1
- package/dist/src/ui/tui/screen-registry.d.ts +0 -7
- package/dist/src/ui/tui/screen-registry.js +13 -4
- package/dist/src/ui/tui/screens/ActivationOptionsScreen.d.ts +2 -2
- package/dist/src/ui/tui/screens/ActivationOptionsScreen.js +8 -8
- package/dist/src/ui/tui/screens/AuthScreen.js +57 -27
- package/dist/src/ui/tui/screens/ChecklistScreen.d.ts +2 -12
- package/dist/src/ui/tui/screens/ChecklistScreen.js +22 -33
- package/dist/src/ui/tui/screens/DataIngestionCheckScreen.d.ts +3 -12
- package/dist/src/ui/tui/screens/DataIngestionCheckScreen.js +109 -39
- package/dist/src/ui/tui/screens/DataSetupScreen.d.ts +3 -3
- package/dist/src/ui/tui/screens/DataSetupScreen.js +17 -10
- package/dist/src/ui/tui/screens/IntroScreen.d.ts +5 -3
- package/dist/src/ui/tui/screens/IntroScreen.js +132 -41
- package/dist/src/ui/tui/screens/LoginScreen.d.ts +1 -1
- package/dist/src/ui/tui/screens/LoginScreen.js +4 -4
- package/dist/src/ui/tui/screens/LogoutScreen.d.ts +4 -2
- package/dist/src/ui/tui/screens/LogoutScreen.js +17 -5
- package/dist/src/ui/tui/screens/McpScreen.d.ts +4 -4
- package/dist/src/ui/tui/screens/McpScreen.js +25 -17
- package/dist/src/ui/tui/screens/OutageScreen.d.ts +1 -1
- package/dist/src/ui/tui/screens/OutageScreen.js +5 -5
- package/dist/src/ui/tui/screens/OutroScreen.d.ts +5 -0
- package/dist/src/ui/tui/screens/OutroScreen.js +21 -14
- package/dist/src/ui/tui/screens/RegionSelectScreen.js +15 -13
- package/dist/src/ui/tui/screens/RunScreen.d.ts +7 -5
- package/dist/src/ui/tui/screens/RunScreen.js +102 -157
- package/dist/src/ui/tui/screens/SettingsOverrideScreen.d.ts +1 -1
- package/dist/src/ui/tui/screens/SettingsOverrideScreen.js +6 -5
- package/dist/src/ui/tui/screens/SetupScreen.d.ts +1 -1
- package/dist/src/ui/tui/screens/SetupScreen.js +7 -7
- package/dist/src/ui/tui/screens/SlackScreen.d.ts +2 -2
- package/dist/src/ui/tui/screens/SlackScreen.js +60 -35
- package/dist/src/ui/tui/session-constants.d.ts +41 -0
- package/dist/src/ui/tui/session-constants.js +38 -0
- package/dist/src/ui/tui/start-tui.d.ts +3 -1
- package/dist/src/ui/tui/start-tui.js +14 -10
- package/dist/src/ui/tui/store.d.ts +2 -1
- package/dist/src/ui/tui/store.js +33 -7
- package/dist/src/ui/tui/styles.d.ts +75 -19
- package/dist/src/ui/tui/styles.js +101 -19
- package/dist/src/ui/tui/utils/classify-error.d.ts +14 -0
- package/dist/src/ui/tui/utils/classify-error.js +90 -0
- package/dist/src/ui/tui/utils/diagnostics.d.ts +21 -0
- package/dist/src/ui/tui/utils/diagnostics.js +72 -0
- package/dist/src/ui/tui/utils/with-retry.d.ts +12 -0
- package/dist/src/ui/tui/utils/with-retry.js +32 -0
- package/dist/src/ui/tui/utils/with-timeout.d.ts +10 -0
- package/dist/src/ui/tui/utils/with-timeout.js +24 -0
- package/dist/src/utils/ampli-settings.d.ts +1 -1
- package/dist/src/utils/ampli-settings.js +15 -5
- package/dist/src/utils/api-key-store.js +5 -5
- package/dist/src/utils/atomic-write.d.ts +15 -0
- package/dist/src/utils/atomic-write.js +34 -0
- package/dist/src/utils/setup-utils.js +2 -2
- package/dist/src/utils/token-refresh.d.ts +22 -0
- package/dist/src/utils/token-refresh.js +79 -0
- package/dist/src/utils/wizard-abort.js +6 -1
- package/package.json +6 -6
- package/skills/instrumentation/add-analytics-instrumentation/SKILL.md +142 -0
- package/skills/instrumentation/diff-intake/SKILL.md +128 -0
- package/skills/instrumentation/discover-analytics-patterns/SKILL.md +185 -0
- package/skills/instrumentation/discover-event-surfaces/SKILL.md +322 -0
- package/skills/instrumentation/discover-event-surfaces/references/best-practices.md +563 -0
- package/skills/instrumentation/instrument-events/SKILL.md +169 -0
- package/skills/instrumentation/instrument-events/references/best-practices.md +563 -0
- package/skills/integration/integration-android/SKILL.md +49 -0
- package/skills/integration/integration-android/references/EXAMPLE.md +1977 -0
- package/skills/integration/integration-android/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-android/references/analytics.md +1778 -0
- package/skills/integration/integration-android/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-android/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-android/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-android/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-angular/SKILL.md +49 -0
- package/skills/integration/integration-angular/references/EXAMPLE.md +899 -0
- package/skills/integration/integration-angular/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-angular/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-angular/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-angular/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-angular/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-angular/references/browser-sdk-2.md +4680 -0
- package/skills/integration/integration-astro-hybrid/SKILL.md +56 -0
- package/skills/integration/integration-astro-hybrid/references/EXAMPLE.md +1095 -0
- package/skills/integration/integration-astro-hybrid/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-astro-hybrid/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-astro-hybrid/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-astro-hybrid/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-astro-hybrid/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-astro-hybrid/references/browser-sdk-2.md +4680 -0
- package/skills/integration/integration-astro-ssr/SKILL.md +52 -0
- package/skills/integration/integration-astro-ssr/references/EXAMPLE.md +1106 -0
- package/skills/integration/integration-astro-ssr/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-astro-ssr/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-astro-ssr/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-astro-ssr/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-astro-ssr/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-astro-ssr/references/browser-sdk-2.md +4680 -0
- package/skills/integration/integration-astro-static/SKILL.md +49 -0
- package/skills/integration/integration-astro-static/references/EXAMPLE.md +910 -0
- package/skills/integration/integration-astro-static/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-astro-static/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-astro-static/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-astro-static/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-astro-static/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-astro-static/references/browser-sdk-2.md +4680 -0
- package/skills/integration/integration-astro-view-transitions/SKILL.md +51 -0
- package/skills/integration/integration-astro-view-transitions/references/EXAMPLE.md +979 -0
- package/skills/integration/integration-astro-view-transitions/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-astro-view-transitions/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-astro-view-transitions/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-astro-view-transitions/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-astro-view-transitions/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-astro-view-transitions/references/browser-sdk-2.md +4680 -0
- package/skills/integration/integration-django/SKILL.md +57 -0
- package/skills/integration/integration-django/references/EXAMPLE.md +1005 -0
- package/skills/integration/integration-django/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-django/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-django/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-django/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-django/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-django/references/python.md +1424 -0
- package/skills/integration/integration-expo/SKILL.md +53 -0
- package/skills/integration/integration-expo/references/EXAMPLE.md +1291 -0
- package/skills/integration/integration-expo/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-expo/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-expo/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-expo/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-expo/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-expo/references/react-native-sdk.md +2819 -0
- package/skills/integration/integration-fastapi/SKILL.md +57 -0
- package/skills/integration/integration-fastapi/references/EXAMPLE.md +1389 -0
- package/skills/integration/integration-fastapi/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-fastapi/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-fastapi/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-fastapi/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-fastapi/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-fastapi/references/python.md +1424 -0
- package/skills/integration/integration-flask/SKILL.md +56 -0
- package/skills/integration/integration-flask/references/EXAMPLE.md +1130 -0
- package/skills/integration/integration-flask/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-flask/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-flask/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-flask/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-flask/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-flask/references/python.md +1424 -0
- package/skills/integration/integration-javascript_node/SKILL.md +54 -0
- package/skills/integration/integration-javascript_node/references/EXAMPLE.md +365 -0
- package/skills/integration/integration-javascript_node/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-javascript_node/references/analytics.md +1778 -0
- package/skills/integration/integration-javascript_node/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-javascript_node/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-javascript_node/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-javascript_node/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-javascript_web/SKILL.md +58 -0
- package/skills/integration/integration-javascript_web/references/EXAMPLE.md +451 -0
- package/skills/integration/integration-javascript_web/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-javascript_web/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-javascript_web/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-javascript_web/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-javascript_web/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-javascript_web/references/browser-sdk-2.md +4680 -0
- package/skills/integration/integration-laravel/SKILL.md +52 -0
- package/skills/integration/integration-laravel/references/EXAMPLE.md +2039 -0
- package/skills/integration/integration-laravel/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-laravel/references/analytics.md +1778 -0
- package/skills/integration/integration-laravel/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-laravel/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-laravel/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-laravel/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-nextjs-app-router/SKILL.md +54 -0
- package/skills/integration/integration-nextjs-app-router/references/EXAMPLE.md +673 -0
- package/skills/integration/integration-nextjs-app-router/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-nextjs-app-router/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-nextjs-app-router/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-nextjs-app-router/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-nextjs-app-router/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-nextjs-app-router/references/browser-sdk-2.md +4680 -0
- package/skills/integration/integration-nextjs-pages-router/SKILL.md +54 -0
- package/skills/integration/integration-nextjs-pages-router/references/EXAMPLE.md +735 -0
- package/skills/integration/integration-nextjs-pages-router/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-nextjs-pages-router/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-nextjs-pages-router/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-nextjs-pages-router/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-nextjs-pages-router/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-nextjs-pages-router/references/browser-sdk-2.md +4680 -0
- package/skills/integration/integration-nuxt-3.6/SKILL.md +46 -0
- package/skills/integration/integration-nuxt-3.6/references/EXAMPLE.md +8422 -0
- package/skills/integration/integration-nuxt-3.6/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-nuxt-3.6/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-nuxt-3.6/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-nuxt-3.6/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-nuxt-3.6/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-nuxt-3.6/references/browser-sdk-2.md +4680 -0
- package/skills/integration/integration-nuxt-4/SKILL.md +46 -0
- package/skills/integration/integration-nuxt-4/references/EXAMPLE.md +8670 -0
- package/skills/integration/integration-nuxt-4/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-nuxt-4/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-nuxt-4/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-nuxt-4/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-nuxt-4/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-nuxt-4/references/browser-sdk-2.md +4680 -0
- package/skills/integration/integration-python/SKILL.md +53 -0
- package/skills/integration/integration-python/references/EXAMPLE.md +445 -0
- package/skills/integration/integration-python/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-python/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-python/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-python/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-python/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-python/references/python.md +1424 -0
- package/skills/integration/integration-react-native/SKILL.md +49 -0
- package/skills/integration/integration-react-native/references/EXAMPLE.md +2253 -0
- package/skills/integration/integration-react-native/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-react-native/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-react-native/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-react-native/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-react-native/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-react-native/references/react-native-sdk.md +2819 -0
- package/skills/integration/integration-react-react-router-6/SKILL.md +53 -0
- package/skills/integration/integration-react-react-router-6/references/EXAMPLE.md +570 -0
- package/skills/integration/integration-react-react-router-6/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-react-react-router-6/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-react-react-router-6/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-react-react-router-6/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-react-react-router-6/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-react-react-router-6/references/browser-sdk-2.md +4680 -0
- package/skills/integration/integration-react-react-router-7-data/SKILL.md +53 -0
- package/skills/integration/integration-react-react-router-7-data/references/EXAMPLE.md +830 -0
- package/skills/integration/integration-react-react-router-7-data/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-react-react-router-7-data/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-react-react-router-7-data/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-react-react-router-7-data/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-react-react-router-7-data/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-react-react-router-7-data/references/browser-sdk-2.md +4680 -0
- package/skills/integration/integration-react-react-router-7-declarative/SKILL.md +53 -0
- package/skills/integration/integration-react-react-router-7-declarative/references/EXAMPLE.md +609 -0
- package/skills/integration/integration-react-react-router-7-declarative/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-react-react-router-7-declarative/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-react-react-router-7-declarative/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-react-react-router-7-declarative/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-react-react-router-7-declarative/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-react-react-router-7-declarative/references/browser-sdk-2.md +4680 -0
- package/skills/integration/integration-react-react-router-7-framework/SKILL.md +53 -0
- package/skills/integration/integration-react-react-router-7-framework/references/EXAMPLE.md +1081 -0
- package/skills/integration/integration-react-react-router-7-framework/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-react-react-router-7-framework/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-react-react-router-7-framework/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-react-react-router-7-framework/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-react-react-router-7-framework/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-react-react-router-7-framework/references/browser-sdk-2.md +4680 -0
- package/skills/integration/integration-react-tanstack-router-code-based/SKILL.md +57 -0
- package/skills/integration/integration-react-tanstack-router-code-based/references/EXAMPLE.md +659 -0
- package/skills/integration/integration-react-tanstack-router-code-based/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-react-tanstack-router-code-based/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-react-tanstack-router-code-based/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-react-tanstack-router-code-based/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-react-tanstack-router-code-based/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-react-tanstack-router-code-based/references/browser-sdk-2.md +4680 -0
- package/skills/integration/integration-react-tanstack-router-file-based/SKILL.md +57 -0
- package/skills/integration/integration-react-tanstack-router-file-based/references/EXAMPLE.md +777 -0
- package/skills/integration/integration-react-tanstack-router-file-based/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-react-tanstack-router-file-based/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-react-tanstack-router-file-based/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-react-tanstack-router-file-based/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-react-tanstack-router-file-based/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-react-tanstack-router-file-based/references/browser-sdk-2.md +4680 -0
- package/skills/integration/integration-react-vite/SKILL.md +53 -0
- package/skills/integration/integration-react-vite/references/EXAMPLE.md +542 -0
- package/skills/integration/integration-react-vite/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-react-vite/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-react-vite/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-react-vite/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-react-vite/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-react-vite/references/browser-sdk-2.md +4680 -0
- package/skills/integration/integration-ruby/SKILL.md +50 -0
- package/skills/integration/integration-ruby/references/EXAMPLE.md +420 -0
- package/skills/integration/integration-ruby/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-ruby/references/analytics.md +1778 -0
- package/skills/integration/integration-ruby/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-ruby/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-ruby/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-ruby/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-ruby-on-rails/SKILL.md +55 -0
- package/skills/integration/integration-ruby-on-rails/references/EXAMPLE.md +1013 -0
- package/skills/integration/integration-ruby-on-rails/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-ruby-on-rails/references/analytics.md +1778 -0
- package/skills/integration/integration-ruby-on-rails/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-ruby-on-rails/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-ruby-on-rails/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-ruby-on-rails/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-sveltekit/SKILL.md +47 -0
- package/skills/integration/integration-sveltekit/references/EXAMPLE.md +14121 -0
- package/skills/integration/integration-sveltekit/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-sveltekit/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-sveltekit/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-sveltekit/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-sveltekit/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-sveltekit/references/browser-sdk-2.md +4680 -0
- package/skills/integration/integration-swift/SKILL.md +49 -0
- package/skills/integration/integration-swift/references/EXAMPLE.md +660 -0
- package/skills/integration/integration-swift/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-swift/references/analytics.md +1778 -0
- package/skills/integration/integration-swift/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-swift/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-swift/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-swift/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-tanstack-start/SKILL.md +58 -0
- package/skills/integration/integration-tanstack-start/references/EXAMPLE.md +998 -0
- package/skills/integration/integration-tanstack-start/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-tanstack-start/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-tanstack-start/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-tanstack-start/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-tanstack-start/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-tanstack-start/references/browser-sdk-2.md +4680 -0
- package/skills/integration/integration-vue-3/SKILL.md +46 -0
- package/skills/integration/integration-vue-3/references/EXAMPLE.md +846 -0
- package/skills/integration/integration-vue-3/references/amplitude-quickstart.md +1845 -0
- package/skills/integration/integration-vue-3/references/basic-integration-1.0-begin.md +43 -0
- package/skills/integration/integration-vue-3/references/basic-integration-1.1-edit.md +35 -0
- package/skills/integration/integration-vue-3/references/basic-integration-1.2-revise.md +23 -0
- package/skills/integration/integration-vue-3/references/basic-integration-1.3-conclude.md +57 -0
- package/skills/integration/integration-vue-3/references/browser-sdk-2.md +4680 -0
- package/skills/taxonomy/amplitude-quickstart-taxonomy-agent/SKILL.md +228 -0
- package/dist/src/ui/tui/components/TitleBar.d.ts +0 -8
- package/dist/src/ui/tui/components/TitleBar.js +0 -27
- package/dist/src/ui/tui/primitives/KagiSmallWebViewer.d.ts +0 -7
- package/dist/src/ui/tui/primitives/KagiSmallWebViewer.js +0 -101
- package/dist/src/utils/anthropic-status.d.ts +0 -17
- package/dist/src/utils/anthropic-status.js +0 -51
|
@@ -0,0 +1,1389 @@
|
|
|
1
|
+
# Amplitude FastAPI Example Project
|
|
2
|
+
|
|
3
|
+
Repository: https://github.com/amplitude/context-hub
|
|
4
|
+
Path: basics/fastapi
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## README.md
|
|
9
|
+
|
|
10
|
+
# Amplitude FastAPI Example
|
|
11
|
+
|
|
12
|
+
A FastAPI application demonstrating Amplitude integration for analytics and event tracking.
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- User registration and authentication with cookie-based sessions
|
|
17
|
+
- SQLite database persistence with SQLAlchemy
|
|
18
|
+
- User identification and property tracking
|
|
19
|
+
- Custom event tracking
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
1. Create and activate a virtual environment:
|
|
24
|
+
```bash
|
|
25
|
+
python -m venv venv
|
|
26
|
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
2. Install dependencies:
|
|
30
|
+
```bash
|
|
31
|
+
pip install -r requirements.txt
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
3. Copy the environment file and configure:
|
|
35
|
+
```bash
|
|
36
|
+
cp .env.example .env
|
|
37
|
+
# Edit .env with your Amplitude API key
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
4. Run the application:
|
|
41
|
+
```bash
|
|
42
|
+
python run.py
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
5. Open http://localhost:5002 and either:
|
|
46
|
+
- Login with default credentials: `admin@example.com` / `admin`
|
|
47
|
+
- Or click "Sign up here" to create a new account
|
|
48
|
+
|
|
49
|
+
## Amplitude Integration Points
|
|
50
|
+
|
|
51
|
+
### User Registration
|
|
52
|
+
New users are identified and tracked on signup:
|
|
53
|
+
```python
|
|
54
|
+
client = get_amplitude_client()
|
|
55
|
+
if client:
|
|
56
|
+
identify_obj = Identify()
|
|
57
|
+
identify_obj.set('email', user.email)
|
|
58
|
+
identify_obj.set('is_staff', user.is_staff)
|
|
59
|
+
identify_obj.set('date_joined', user.date_joined.isoformat())
|
|
60
|
+
client.identify(identify_obj, {'user_id': user.email})
|
|
61
|
+
|
|
62
|
+
client.track(BaseEvent(
|
|
63
|
+
event_type='User Signed Up',
|
|
64
|
+
user_id=user.email,
|
|
65
|
+
event_properties={'signup_method': 'form'},
|
|
66
|
+
))
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### User Identification
|
|
70
|
+
Users are identified on login with their properties:
|
|
71
|
+
```python
|
|
72
|
+
client = get_amplitude_client()
|
|
73
|
+
if client:
|
|
74
|
+
identify_obj = Identify()
|
|
75
|
+
identify_obj.set('email', user.email)
|
|
76
|
+
identify_obj.set('is_staff', user.is_staff)
|
|
77
|
+
client.identify(identify_obj, {'user_id': user.email})
|
|
78
|
+
|
|
79
|
+
client.track(BaseEvent(
|
|
80
|
+
event_type='User Logged In',
|
|
81
|
+
user_id=user.email,
|
|
82
|
+
event_properties={'login_method': 'password'},
|
|
83
|
+
))
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Event Tracking
|
|
87
|
+
Custom events are tracked throughout the app:
|
|
88
|
+
```python
|
|
89
|
+
client = get_amplitude_client()
|
|
90
|
+
if client:
|
|
91
|
+
client.track(BaseEvent(
|
|
92
|
+
event_type='Burrito Considered',
|
|
93
|
+
user_id=current_user.email,
|
|
94
|
+
event_properties={'total_considerations': new_count},
|
|
95
|
+
))
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Project Structure
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
basics/fastapi/
|
|
102
|
+
├── app/
|
|
103
|
+
│ ├── __init__.py # Package marker
|
|
104
|
+
│ ├── config.py # Pydantic Settings configuration
|
|
105
|
+
│ ├── database.py # SQLAlchemy setup
|
|
106
|
+
│ ├── dependencies.py # FastAPI dependency injection
|
|
107
|
+
│ ├── main.py # Application factory and lifespan
|
|
108
|
+
│ ├── middleware.py # Amplitude client helper
|
|
109
|
+
│ ├── models.py # User model (SQLAlchemy)
|
|
110
|
+
│ ├── routers/
|
|
111
|
+
│ │ ├── __init__.py # Routers package
|
|
112
|
+
│ │ ├── main.py # Page routes (HTML)
|
|
113
|
+
│ │ └── api.py # API endpoints (JSON)
|
|
114
|
+
│ └── templates/ # Jinja2 templates
|
|
115
|
+
├── .env.example
|
|
116
|
+
├── .gitignore
|
|
117
|
+
├── requirements.txt
|
|
118
|
+
├── README.md
|
|
119
|
+
└── run.py # Entry point (uvicorn)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## .env.example
|
|
125
|
+
|
|
126
|
+
```example
|
|
127
|
+
AMPLITUDE_API_KEY=your_amplitude_api_key_here
|
|
128
|
+
SECRET_KEY=your-secret-key-here
|
|
129
|
+
DEBUG=True
|
|
130
|
+
AMPLITUDE_DISABLED=False
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## app/__init__.py
|
|
137
|
+
|
|
138
|
+
```py
|
|
139
|
+
"""FastAPI Amplitude example application."""
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## app/config.py
|
|
146
|
+
|
|
147
|
+
```py
|
|
148
|
+
"""FastAPI application configuration using Pydantic Settings."""
|
|
149
|
+
|
|
150
|
+
from functools import lru_cache
|
|
151
|
+
|
|
152
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class Settings(BaseSettings):
|
|
156
|
+
"""Application settings loaded from environment variables."""
|
|
157
|
+
|
|
158
|
+
model_config = SettingsConfigDict(
|
|
159
|
+
env_file=".env",
|
|
160
|
+
env_file_encoding="utf-8",
|
|
161
|
+
extra="ignore",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Application
|
|
165
|
+
secret_key: str = "dev-secret-key-change-in-production"
|
|
166
|
+
debug: bool = True
|
|
167
|
+
|
|
168
|
+
# Database (SQLite like Flask example)
|
|
169
|
+
database_url: str = "sqlite:///./db.sqlite3"
|
|
170
|
+
|
|
171
|
+
# Amplitude
|
|
172
|
+
amplitude_api_key: str = ""
|
|
173
|
+
amplitude_disabled: bool = False
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@lru_cache
|
|
177
|
+
def get_settings() -> Settings:
|
|
178
|
+
"""Get cached settings instance."""
|
|
179
|
+
return Settings()
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## app/database.py
|
|
186
|
+
|
|
187
|
+
```py
|
|
188
|
+
"""Database configuration with SQLAlchemy."""
|
|
189
|
+
|
|
190
|
+
from sqlalchemy import create_engine
|
|
191
|
+
from sqlalchemy.orm import DeclarativeBase, sessionmaker
|
|
192
|
+
|
|
193
|
+
from app.config import get_settings
|
|
194
|
+
|
|
195
|
+
settings = get_settings()
|
|
196
|
+
|
|
197
|
+
engine = create_engine(
|
|
198
|
+
settings.database_url,
|
|
199
|
+
connect_args={"check_same_thread": False}, # Required for SQLite
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class Base(DeclarativeBase):
|
|
206
|
+
"""Base class for SQLAlchemy models."""
|
|
207
|
+
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def get_db():
|
|
212
|
+
"""Dependency that provides a database session."""
|
|
213
|
+
db = SessionLocal()
|
|
214
|
+
try:
|
|
215
|
+
yield db
|
|
216
|
+
finally:
|
|
217
|
+
db.close()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def init_db():
|
|
221
|
+
"""Create all database tables."""
|
|
222
|
+
Base.metadata.create_all(bind=engine)
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## app/dependencies.py
|
|
229
|
+
|
|
230
|
+
```py
|
|
231
|
+
"""Authentication dependencies for FastAPI."""
|
|
232
|
+
|
|
233
|
+
from typing import Annotated, Optional
|
|
234
|
+
|
|
235
|
+
from fastapi import Cookie, Depends, HTTPException, status
|
|
236
|
+
from itsdangerous import BadSignature, URLSafeSerializer
|
|
237
|
+
from sqlalchemy.orm import Session
|
|
238
|
+
|
|
239
|
+
from app.config import get_settings
|
|
240
|
+
from app.database import get_db
|
|
241
|
+
from app.models import User
|
|
242
|
+
|
|
243
|
+
settings = get_settings()
|
|
244
|
+
serializer = URLSafeSerializer(settings.secret_key)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def get_session_user_id(session_token: Annotated[Optional[str], Cookie()] = None) -> Optional[int]:
|
|
248
|
+
"""Extract user ID from session cookie."""
|
|
249
|
+
if not session_token:
|
|
250
|
+
return None
|
|
251
|
+
try:
|
|
252
|
+
data = serializer.loads(session_token)
|
|
253
|
+
return data.get("user_id")
|
|
254
|
+
except BadSignature:
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def get_current_user(
|
|
259
|
+
db: Annotated[Session, Depends(get_db)],
|
|
260
|
+
user_id: Annotated[Optional[int], Depends(get_session_user_id)],
|
|
261
|
+
) -> Optional[User]:
|
|
262
|
+
"""Get the current authenticated user, or None if not authenticated."""
|
|
263
|
+
if user_id is None:
|
|
264
|
+
return None
|
|
265
|
+
return User.get_by_id(db, user_id)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def require_auth(
|
|
269
|
+
current_user: Annotated[Optional[User], Depends(get_current_user)],
|
|
270
|
+
) -> User:
|
|
271
|
+
"""Require authentication - raises 401 if not authenticated."""
|
|
272
|
+
if current_user is None:
|
|
273
|
+
raise HTTPException(
|
|
274
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
275
|
+
detail="Authentication required",
|
|
276
|
+
)
|
|
277
|
+
return current_user
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def create_session_token(user_id: int) -> str:
|
|
281
|
+
"""Create a signed session token for the user."""
|
|
282
|
+
return serializer.dumps({"user_id": user_id})
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# Type aliases for cleaner dependency injection
|
|
286
|
+
CurrentUser = Annotated[Optional[User], Depends(get_current_user)]
|
|
287
|
+
RequiredUser = Annotated[User, Depends(require_auth)]
|
|
288
|
+
DbSession = Annotated[Session, Depends(get_db)]
|
|
289
|
+
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## app/main.py
|
|
295
|
+
|
|
296
|
+
```py
|
|
297
|
+
"""FastAPI application with Amplitude integration."""
|
|
298
|
+
|
|
299
|
+
from contextlib import asynccontextmanager
|
|
300
|
+
from pathlib import Path
|
|
301
|
+
|
|
302
|
+
from fastapi import FastAPI, Request
|
|
303
|
+
from fastapi.responses import HTMLResponse, JSONResponse
|
|
304
|
+
from fastapi.templating import Jinja2Templates
|
|
305
|
+
|
|
306
|
+
from app.config import get_settings
|
|
307
|
+
from app.database import SessionLocal, init_db
|
|
308
|
+
from app.models import User
|
|
309
|
+
from app.routers import api, main
|
|
310
|
+
|
|
311
|
+
settings = get_settings()
|
|
312
|
+
|
|
313
|
+
# Setup templates
|
|
314
|
+
templates_dir = Path(__file__).parent / "templates"
|
|
315
|
+
templates = Jinja2Templates(directory=str(templates_dir))
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@asynccontextmanager
|
|
319
|
+
async def lifespan(app: FastAPI):
|
|
320
|
+
"""Application lifespan events for startup/shutdown."""
|
|
321
|
+
# Initialize database and seed default user
|
|
322
|
+
init_db()
|
|
323
|
+
db = SessionLocal()
|
|
324
|
+
try:
|
|
325
|
+
if not User.get_by_email(db, "admin@example.com"):
|
|
326
|
+
User.create_user(
|
|
327
|
+
db,
|
|
328
|
+
email="admin@example.com",
|
|
329
|
+
password="admin",
|
|
330
|
+
is_staff=True,
|
|
331
|
+
)
|
|
332
|
+
finally:
|
|
333
|
+
db.close()
|
|
334
|
+
|
|
335
|
+
yield
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
app = FastAPI(
|
|
339
|
+
title="Amplitude FastAPI Example",
|
|
340
|
+
description="Example application demonstrating Amplitude integration with FastAPI",
|
|
341
|
+
lifespan=lifespan,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
# Include routers
|
|
345
|
+
app.include_router(main.router)
|
|
346
|
+
app.include_router(api.router, prefix="/api")
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# Error handlers
|
|
350
|
+
@app.exception_handler(404)
|
|
351
|
+
async def not_found_handler(request: Request, exc):
|
|
352
|
+
"""Handle 404 errors."""
|
|
353
|
+
if request.url.path.startswith("/api/"):
|
|
354
|
+
return JSONResponse({"error": "Not found"}, status_code=404)
|
|
355
|
+
return templates.TemplateResponse(
|
|
356
|
+
request, "errors/404.html", status_code=404
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@app.exception_handler(500)
|
|
361
|
+
async def internal_error_handler(request: Request, exc):
|
|
362
|
+
"""Handle 500 errors."""
|
|
363
|
+
if request.url.path.startswith("/api/"):
|
|
364
|
+
return JSONResponse({"error": "Internal server error"}, status_code=500)
|
|
365
|
+
return templates.TemplateResponse(
|
|
366
|
+
request, "errors/500.html", status_code=500
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## app/middleware.py
|
|
374
|
+
|
|
375
|
+
```py
|
|
376
|
+
"""Amplitude helper for FastAPI request tracking."""
|
|
377
|
+
|
|
378
|
+
from amplitude import Amplitude
|
|
379
|
+
|
|
380
|
+
from app.config import get_settings
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def get_amplitude_client():
|
|
384
|
+
"""Get the Amplitude client instance, or None if disabled."""
|
|
385
|
+
settings = get_settings()
|
|
386
|
+
if not settings.amplitude_api_key or settings.amplitude_disabled:
|
|
387
|
+
return None
|
|
388
|
+
return Amplitude(settings.amplitude_api_key)
|
|
389
|
+
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## app/models.py
|
|
395
|
+
|
|
396
|
+
```py
|
|
397
|
+
"""User model with SQLite persistence (similar to Flask example)."""
|
|
398
|
+
|
|
399
|
+
from datetime import datetime, timezone
|
|
400
|
+
from typing import Optional
|
|
401
|
+
|
|
402
|
+
from sqlalchemy import Boolean, DateTime, Integer, String
|
|
403
|
+
from sqlalchemy.orm import Mapped, Session, mapped_column
|
|
404
|
+
from werkzeug.security import check_password_hash, generate_password_hash
|
|
405
|
+
|
|
406
|
+
from app.database import Base
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class User(Base):
|
|
410
|
+
"""User model with SQLite persistence."""
|
|
411
|
+
|
|
412
|
+
__tablename__ = "users"
|
|
413
|
+
|
|
414
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
415
|
+
email: Mapped[str] = mapped_column(String(254), unique=True, nullable=False)
|
|
416
|
+
password_hash: Mapped[str] = mapped_column(String(256), nullable=False)
|
|
417
|
+
name: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
|
|
418
|
+
is_staff: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
419
|
+
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
420
|
+
login_count: Mapped[int] = mapped_column(Integer, default=0)
|
|
421
|
+
date_joined: Mapped[datetime] = mapped_column(
|
|
422
|
+
DateTime, default=lambda: datetime.now(timezone.utc)
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
def set_password(self, password: str) -> None:
|
|
426
|
+
"""Hash and set the user's password."""
|
|
427
|
+
self.password_hash = generate_password_hash(password, method="pbkdf2:sha256")
|
|
428
|
+
|
|
429
|
+
def check_password(self, password: str) -> bool:
|
|
430
|
+
"""Verify the password against the hash."""
|
|
431
|
+
return check_password_hash(self.password_hash, password)
|
|
432
|
+
|
|
433
|
+
@classmethod
|
|
434
|
+
def create_user(
|
|
435
|
+
cls, db: Session, email: str, password: str, is_staff: bool = False
|
|
436
|
+
) -> "User":
|
|
437
|
+
"""Create and save a new user."""
|
|
438
|
+
user = cls(email=email, is_staff=is_staff)
|
|
439
|
+
# nosemgrep: python.django.security.audit.unvalidated-password.unvalidated-password
|
|
440
|
+
user.set_password(password)
|
|
441
|
+
db.add(user)
|
|
442
|
+
db.commit()
|
|
443
|
+
db.refresh(user)
|
|
444
|
+
return user
|
|
445
|
+
|
|
446
|
+
@classmethod
|
|
447
|
+
def get_by_id(cls, db: Session, user_id: int) -> Optional["User"]:
|
|
448
|
+
"""Get user by ID."""
|
|
449
|
+
return db.query(cls).filter(cls.id == user_id).first()
|
|
450
|
+
|
|
451
|
+
@classmethod
|
|
452
|
+
def get_by_email(cls, db: Session, email: str) -> Optional["User"]:
|
|
453
|
+
"""Get user by email."""
|
|
454
|
+
return db.query(cls).filter(cls.email == email).first()
|
|
455
|
+
|
|
456
|
+
@classmethod
|
|
457
|
+
def authenticate(cls, db: Session, email: str, password: str) -> Optional["User"]:
|
|
458
|
+
"""Authenticate user with email and password."""
|
|
459
|
+
user = cls.get_by_email(db, email)
|
|
460
|
+
if user and user.check_password(password):
|
|
461
|
+
return user
|
|
462
|
+
return None
|
|
463
|
+
|
|
464
|
+
def record_login(self, db: Session) -> bool:
|
|
465
|
+
"""Record a login and return whether this is the user's first login."""
|
|
466
|
+
is_first_login = self.login_count == 0
|
|
467
|
+
self.login_count += 1
|
|
468
|
+
db.commit()
|
|
469
|
+
return is_first_login
|
|
470
|
+
|
|
471
|
+
def update_profile(self, db: Session, name: Optional[str] = None) -> list:
|
|
472
|
+
"""Update user profile and return list of changed fields."""
|
|
473
|
+
changed_fields = []
|
|
474
|
+
if name is not None and name != self.name:
|
|
475
|
+
self.name = name
|
|
476
|
+
changed_fields.append("name")
|
|
477
|
+
if changed_fields:
|
|
478
|
+
db.commit()
|
|
479
|
+
return changed_fields
|
|
480
|
+
|
|
481
|
+
def __repr__(self) -> str:
|
|
482
|
+
return f"<User {self.email}>"
|
|
483
|
+
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
## app/routers/__init__.py
|
|
489
|
+
|
|
490
|
+
```py
|
|
491
|
+
"""FastAPI routers package."""
|
|
492
|
+
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
## app/routers/api.py
|
|
498
|
+
|
|
499
|
+
```py
|
|
500
|
+
"""API endpoints demonstrating Amplitude integration patterns."""
|
|
501
|
+
|
|
502
|
+
from typing import Annotated
|
|
503
|
+
|
|
504
|
+
from amplitude import BaseEvent
|
|
505
|
+
from fastapi import APIRouter, Cookie, Form, Query
|
|
506
|
+
from fastapi.responses import JSONResponse
|
|
507
|
+
|
|
508
|
+
from app.dependencies import RequiredUser
|
|
509
|
+
from app.middleware import get_amplitude_client
|
|
510
|
+
|
|
511
|
+
router = APIRouter()
|
|
512
|
+
|
|
513
|
+
MAX_BURRITO_COUNT = 10000
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
@router.post("/burrito/consider")
|
|
517
|
+
async def consider_burrito(
|
|
518
|
+
current_user: RequiredUser,
|
|
519
|
+
burrito_count: Annotated[int, Cookie()] = 0,
|
|
520
|
+
):
|
|
521
|
+
"""Track burrito consideration event."""
|
|
522
|
+
safe_count = max(0, min(burrito_count, MAX_BURRITO_COUNT))
|
|
523
|
+
new_count = safe_count + 1
|
|
524
|
+
|
|
525
|
+
# Amplitude: Capture custom event
|
|
526
|
+
client = get_amplitude_client()
|
|
527
|
+
if client:
|
|
528
|
+
client.track(BaseEvent(
|
|
529
|
+
event_type="Burrito Considered",
|
|
530
|
+
user_id=current_user.email,
|
|
531
|
+
event_properties={"total_considerations": new_count},
|
|
532
|
+
))
|
|
533
|
+
|
|
534
|
+
response = JSONResponse({"success": True, "count": new_count})
|
|
535
|
+
response.set_cookie(
|
|
536
|
+
key="burrito_count",
|
|
537
|
+
value=str(new_count),
|
|
538
|
+
httponly=True,
|
|
539
|
+
samesite="lax",
|
|
540
|
+
)
|
|
541
|
+
return response
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
@router.post("/test-error")
|
|
545
|
+
async def test_error(
|
|
546
|
+
current_user: RequiredUser,
|
|
547
|
+
capture_param: Annotated[str, Query(alias="capture")] = "true",
|
|
548
|
+
):
|
|
549
|
+
"""Test endpoint demonstrating manual error event capture in Amplitude."""
|
|
550
|
+
should_capture = capture_param.lower() == "true"
|
|
551
|
+
|
|
552
|
+
try:
|
|
553
|
+
raise Exception("Test exception from critical operation")
|
|
554
|
+
except Exception as e:
|
|
555
|
+
if should_capture:
|
|
556
|
+
client = get_amplitude_client()
|
|
557
|
+
if client:
|
|
558
|
+
client.track(BaseEvent(
|
|
559
|
+
event_type="Error Occurred",
|
|
560
|
+
user_id=current_user.email,
|
|
561
|
+
event_properties={
|
|
562
|
+
"error_message": str(e),
|
|
563
|
+
"error_type": type(e).__name__,
|
|
564
|
+
},
|
|
565
|
+
))
|
|
566
|
+
return JSONResponse(
|
|
567
|
+
{
|
|
568
|
+
"error": "Operation failed",
|
|
569
|
+
"message": f"Error captured in Amplitude: {str(e)}",
|
|
570
|
+
},
|
|
571
|
+
status_code=500,
|
|
572
|
+
)
|
|
573
|
+
else:
|
|
574
|
+
return JSONResponse({"error": "Operation failed"}, status_code=500)
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
@router.post("/reports/activity")
|
|
578
|
+
async def generate_activity_report(
|
|
579
|
+
current_user: RequiredUser,
|
|
580
|
+
report_type: Annotated[str, Form()] = "summary",
|
|
581
|
+
):
|
|
582
|
+
"""Generate user activity report."""
|
|
583
|
+
valid_report_types = {"summary", "detailed", "export"}
|
|
584
|
+
safe_report_type = report_type if report_type in valid_report_types else "summary"
|
|
585
|
+
|
|
586
|
+
report_data = {
|
|
587
|
+
"user": current_user.email,
|
|
588
|
+
"date_joined": current_user.date_joined.isoformat(),
|
|
589
|
+
"login_count": current_user.login_count,
|
|
590
|
+
"is_staff": current_user.is_staff,
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if safe_report_type == "detailed":
|
|
594
|
+
report_data["account_age_days"] = (
|
|
595
|
+
__import__("datetime").datetime.now(__import__("datetime").timezone.utc)
|
|
596
|
+
- current_user.date_joined
|
|
597
|
+
).days
|
|
598
|
+
|
|
599
|
+
row_count = len(report_data)
|
|
600
|
+
|
|
601
|
+
# Amplitude: Track report generation
|
|
602
|
+
client = get_amplitude_client()
|
|
603
|
+
if client:
|
|
604
|
+
client.track(BaseEvent(
|
|
605
|
+
event_type="Report Generated",
|
|
606
|
+
user_id=current_user.email,
|
|
607
|
+
event_properties={
|
|
608
|
+
"report_type": safe_report_type,
|
|
609
|
+
"row_count": row_count,
|
|
610
|
+
},
|
|
611
|
+
))
|
|
612
|
+
|
|
613
|
+
return JSONResponse(
|
|
614
|
+
{
|
|
615
|
+
"success": True,
|
|
616
|
+
"report_type": safe_report_type,
|
|
617
|
+
"row_count": row_count,
|
|
618
|
+
"data": report_data,
|
|
619
|
+
}
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
## app/routers/main.py
|
|
627
|
+
|
|
628
|
+
```py
|
|
629
|
+
"""Main routes demonstrating Amplitude integration patterns."""
|
|
630
|
+
|
|
631
|
+
from pathlib import Path
|
|
632
|
+
from typing import Annotated
|
|
633
|
+
|
|
634
|
+
from amplitude import BaseEvent, Identify
|
|
635
|
+
from fastapi import APIRouter, Cookie, Depends, Form, Request
|
|
636
|
+
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
637
|
+
from fastapi.templating import Jinja2Templates
|
|
638
|
+
|
|
639
|
+
from app.dependencies import (
|
|
640
|
+
CurrentUser,
|
|
641
|
+
DbSession,
|
|
642
|
+
RequiredUser,
|
|
643
|
+
create_session_token,
|
|
644
|
+
)
|
|
645
|
+
from app.middleware import get_amplitude_client
|
|
646
|
+
from app.models import User
|
|
647
|
+
|
|
648
|
+
router = APIRouter()
|
|
649
|
+
|
|
650
|
+
# Setup templates
|
|
651
|
+
templates_dir = Path(__file__).parent.parent / "templates"
|
|
652
|
+
templates = Jinja2Templates(directory=str(templates_dir))
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
@router.get("/", response_class=HTMLResponse)
|
|
656
|
+
async def home(request: Request, current_user: CurrentUser, db: DbSession):
|
|
657
|
+
"""Home/login page."""
|
|
658
|
+
if current_user:
|
|
659
|
+
return RedirectResponse(url="/dashboard", status_code=302)
|
|
660
|
+
|
|
661
|
+
return templates.TemplateResponse(
|
|
662
|
+
request, "home.html", {"current_user": current_user}
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
@router.post("/", response_class=HTMLResponse)
|
|
667
|
+
async def login(
|
|
668
|
+
request: Request,
|
|
669
|
+
db: DbSession,
|
|
670
|
+
email: Annotated[str, Form()],
|
|
671
|
+
password: Annotated[str, Form()],
|
|
672
|
+
):
|
|
673
|
+
"""Handle login form submission."""
|
|
674
|
+
user = User.authenticate(db, email, password)
|
|
675
|
+
|
|
676
|
+
if user:
|
|
677
|
+
user.record_login(db)
|
|
678
|
+
|
|
679
|
+
# Amplitude: Identify user and capture login event
|
|
680
|
+
client = get_amplitude_client()
|
|
681
|
+
if client:
|
|
682
|
+
identify_obj = Identify()
|
|
683
|
+
identify_obj.set("email", user.email)
|
|
684
|
+
identify_obj.set("is_staff", user.is_staff)
|
|
685
|
+
client.identify(identify_obj, {"user_id": user.email})
|
|
686
|
+
|
|
687
|
+
client.track(BaseEvent(
|
|
688
|
+
event_type="User Logged In",
|
|
689
|
+
user_id=user.email,
|
|
690
|
+
event_properties={"login_method": "password"},
|
|
691
|
+
))
|
|
692
|
+
|
|
693
|
+
# Create session and redirect
|
|
694
|
+
response = RedirectResponse(url="/dashboard", status_code=302)
|
|
695
|
+
response.set_cookie(
|
|
696
|
+
key="session_token",
|
|
697
|
+
value=create_session_token(user.id),
|
|
698
|
+
httponly=True,
|
|
699
|
+
samesite="lax",
|
|
700
|
+
)
|
|
701
|
+
return response
|
|
702
|
+
|
|
703
|
+
# Login failed
|
|
704
|
+
return templates.TemplateResponse(
|
|
705
|
+
request,
|
|
706
|
+
"home.html",
|
|
707
|
+
{"current_user": None, "error": "Invalid email or password"},
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
@router.get("/signup", response_class=HTMLResponse)
|
|
712
|
+
async def signup_page(request: Request, current_user: CurrentUser):
|
|
713
|
+
"""User registration page."""
|
|
714
|
+
if current_user:
|
|
715
|
+
return RedirectResponse(url="/dashboard", status_code=302)
|
|
716
|
+
|
|
717
|
+
return templates.TemplateResponse(
|
|
718
|
+
request, "signup.html", {"current_user": current_user}
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
@router.post("/signup", response_class=HTMLResponse)
|
|
723
|
+
async def signup(
|
|
724
|
+
request: Request,
|
|
725
|
+
db: DbSession,
|
|
726
|
+
email: Annotated[str, Form()],
|
|
727
|
+
password: Annotated[str, Form()],
|
|
728
|
+
password_confirm: Annotated[str, Form()],
|
|
729
|
+
):
|
|
730
|
+
"""Handle signup form submission."""
|
|
731
|
+
error = None
|
|
732
|
+
|
|
733
|
+
if not email or not password:
|
|
734
|
+
error = "Email and password are required"
|
|
735
|
+
elif password != password_confirm:
|
|
736
|
+
error = "Passwords do not match"
|
|
737
|
+
elif User.get_by_email(db, email):
|
|
738
|
+
error = "Email already registered"
|
|
739
|
+
|
|
740
|
+
if error:
|
|
741
|
+
return templates.TemplateResponse(
|
|
742
|
+
request, "signup.html", {"current_user": None, "error": error}
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
# Create new user
|
|
746
|
+
user = User.create_user(db, email=email, password=password, is_staff=False)
|
|
747
|
+
|
|
748
|
+
# Amplitude: Identify new user and capture signup event
|
|
749
|
+
client = get_amplitude_client()
|
|
750
|
+
if client:
|
|
751
|
+
identify_obj = Identify()
|
|
752
|
+
identify_obj.set("email", user.email)
|
|
753
|
+
identify_obj.set("is_staff", user.is_staff)
|
|
754
|
+
identify_obj.set("date_joined", user.date_joined.isoformat())
|
|
755
|
+
client.identify(identify_obj, {"user_id": user.email})
|
|
756
|
+
|
|
757
|
+
client.track(BaseEvent(
|
|
758
|
+
event_type="User Signed Up",
|
|
759
|
+
user_id=user.email,
|
|
760
|
+
event_properties={"signup_method": "form"},
|
|
761
|
+
))
|
|
762
|
+
|
|
763
|
+
# Create session and redirect
|
|
764
|
+
response = RedirectResponse(url="/dashboard", status_code=302)
|
|
765
|
+
response.set_cookie(
|
|
766
|
+
key="session_token",
|
|
767
|
+
value=create_session_token(user.id),
|
|
768
|
+
httponly=True,
|
|
769
|
+
samesite="lax",
|
|
770
|
+
)
|
|
771
|
+
return response
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
@router.get("/logout")
|
|
775
|
+
async def logout(current_user: RequiredUser):
|
|
776
|
+
"""Logout and capture event."""
|
|
777
|
+
# Amplitude: Capture logout event
|
|
778
|
+
client = get_amplitude_client()
|
|
779
|
+
if client:
|
|
780
|
+
client.track(BaseEvent(
|
|
781
|
+
event_type="User Logged Out",
|
|
782
|
+
user_id=current_user.email,
|
|
783
|
+
))
|
|
784
|
+
|
|
785
|
+
response = RedirectResponse(url="/", status_code=302)
|
|
786
|
+
response.delete_cookie(key="session_token")
|
|
787
|
+
return response
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
@router.get("/dashboard", response_class=HTMLResponse)
|
|
791
|
+
async def dashboard(
|
|
792
|
+
request: Request,
|
|
793
|
+
current_user: RequiredUser,
|
|
794
|
+
):
|
|
795
|
+
"""Dashboard page."""
|
|
796
|
+
# Amplitude: Capture dashboard view
|
|
797
|
+
client = get_amplitude_client()
|
|
798
|
+
if client:
|
|
799
|
+
client.track(BaseEvent(
|
|
800
|
+
event_type="Dashboard Viewed",
|
|
801
|
+
user_id=current_user.email,
|
|
802
|
+
event_properties={"is_staff": current_user.is_staff},
|
|
803
|
+
))
|
|
804
|
+
|
|
805
|
+
# TODO: Use Amplitude Experiment for feature flags
|
|
806
|
+
|
|
807
|
+
return templates.TemplateResponse(
|
|
808
|
+
request,
|
|
809
|
+
"dashboard.html",
|
|
810
|
+
{"current_user": current_user},
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
@router.get("/burrito", response_class=HTMLResponse)
|
|
815
|
+
async def burrito(
|
|
816
|
+
request: Request,
|
|
817
|
+
current_user: RequiredUser,
|
|
818
|
+
burrito_count: Annotated[int, Cookie()] = 0,
|
|
819
|
+
):
|
|
820
|
+
"""Burrito consideration tracker page."""
|
|
821
|
+
return templates.TemplateResponse(
|
|
822
|
+
request,
|
|
823
|
+
"burrito.html",
|
|
824
|
+
{"current_user": current_user, "burrito_count": burrito_count},
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
@router.get("/profile", response_class=HTMLResponse)
|
|
829
|
+
async def profile(request: Request, current_user: RequiredUser):
|
|
830
|
+
"""User profile page."""
|
|
831
|
+
# Amplitude: Capture profile view
|
|
832
|
+
client = get_amplitude_client()
|
|
833
|
+
if client:
|
|
834
|
+
client.track(BaseEvent(
|
|
835
|
+
event_type="Profile Viewed",
|
|
836
|
+
user_id=current_user.email,
|
|
837
|
+
))
|
|
838
|
+
|
|
839
|
+
return templates.TemplateResponse(
|
|
840
|
+
request, "profile.html", {"current_user": current_user}
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
---
|
|
846
|
+
|
|
847
|
+
## app/templates/base.html
|
|
848
|
+
|
|
849
|
+
```html
|
|
850
|
+
<!DOCTYPE html>
|
|
851
|
+
<html lang="en">
|
|
852
|
+
<head>
|
|
853
|
+
<meta charset="UTF-8">
|
|
854
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
855
|
+
<title>{% block title %}Amplitude FastAPI Example{% endblock %}</title>
|
|
856
|
+
<style>
|
|
857
|
+
* {
|
|
858
|
+
box-sizing: border-box;
|
|
859
|
+
margin: 0;
|
|
860
|
+
padding: 0;
|
|
861
|
+
}
|
|
862
|
+
body {
|
|
863
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
864
|
+
line-height: 1.6;
|
|
865
|
+
background-color: #f5f5f5;
|
|
866
|
+
color: #333;
|
|
867
|
+
}
|
|
868
|
+
.container {
|
|
869
|
+
max-width: 800px;
|
|
870
|
+
margin: 0 auto;
|
|
871
|
+
padding: 20px;
|
|
872
|
+
}
|
|
873
|
+
nav {
|
|
874
|
+
background: #1d4ed8;
|
|
875
|
+
padding: 15px 20px;
|
|
876
|
+
margin-bottom: 30px;
|
|
877
|
+
}
|
|
878
|
+
nav a {
|
|
879
|
+
color: white;
|
|
880
|
+
text-decoration: none;
|
|
881
|
+
margin-right: 20px;
|
|
882
|
+
}
|
|
883
|
+
nav a:hover {
|
|
884
|
+
text-decoration: underline;
|
|
885
|
+
}
|
|
886
|
+
.card {
|
|
887
|
+
background: white;
|
|
888
|
+
border-radius: 8px;
|
|
889
|
+
padding: 20px;
|
|
890
|
+
margin-bottom: 20px;
|
|
891
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
892
|
+
}
|
|
893
|
+
h1, h2, h3 {
|
|
894
|
+
margin-bottom: 15px;
|
|
895
|
+
color: #1d4ed8;
|
|
896
|
+
}
|
|
897
|
+
button, .btn {
|
|
898
|
+
background: #1d4ed8;
|
|
899
|
+
color: white;
|
|
900
|
+
border: none;
|
|
901
|
+
padding: 10px 20px;
|
|
902
|
+
border-radius: 5px;
|
|
903
|
+
cursor: pointer;
|
|
904
|
+
font-size: 14px;
|
|
905
|
+
display: inline-block;
|
|
906
|
+
text-decoration: none;
|
|
907
|
+
}
|
|
908
|
+
button:hover, .btn:hover {
|
|
909
|
+
background: #1e40af;
|
|
910
|
+
}
|
|
911
|
+
button.danger {
|
|
912
|
+
background: #dc2626;
|
|
913
|
+
}
|
|
914
|
+
button.danger:hover {
|
|
915
|
+
background: #b91c1c;
|
|
916
|
+
}
|
|
917
|
+
input {
|
|
918
|
+
width: 100%;
|
|
919
|
+
padding: 10px;
|
|
920
|
+
margin-bottom: 15px;
|
|
921
|
+
border: 1px solid #ddd;
|
|
922
|
+
border-radius: 5px;
|
|
923
|
+
font-size: 14px;
|
|
924
|
+
}
|
|
925
|
+
.messages {
|
|
926
|
+
margin-bottom: 20px;
|
|
927
|
+
}
|
|
928
|
+
.message {
|
|
929
|
+
padding: 10px 15px;
|
|
930
|
+
border-radius: 5px;
|
|
931
|
+
margin-bottom: 10px;
|
|
932
|
+
}
|
|
933
|
+
.message.error {
|
|
934
|
+
background: #fee2e2;
|
|
935
|
+
color: #dc2626;
|
|
936
|
+
}
|
|
937
|
+
.message.success {
|
|
938
|
+
background: #d1fae5;
|
|
939
|
+
color: #059669;
|
|
940
|
+
}
|
|
941
|
+
.feature-flag {
|
|
942
|
+
background: #fef3c7;
|
|
943
|
+
border: 2px dashed #f59e0b;
|
|
944
|
+
padding: 15px;
|
|
945
|
+
border-radius: 8px;
|
|
946
|
+
margin: 20px 0;
|
|
947
|
+
}
|
|
948
|
+
code {
|
|
949
|
+
background: #f3f4f6;
|
|
950
|
+
padding: 2px 6px;
|
|
951
|
+
border-radius: 3px;
|
|
952
|
+
font-family: monospace;
|
|
953
|
+
}
|
|
954
|
+
pre {
|
|
955
|
+
background: #1e293b;
|
|
956
|
+
color: #e2e8f0;
|
|
957
|
+
padding: 16px;
|
|
958
|
+
border-radius: 8px;
|
|
959
|
+
overflow-x: auto;
|
|
960
|
+
font-size: 13px;
|
|
961
|
+
}
|
|
962
|
+
.count {
|
|
963
|
+
font-size: 48px;
|
|
964
|
+
font-weight: bold;
|
|
965
|
+
color: #1d4ed8;
|
|
966
|
+
text-align: center;
|
|
967
|
+
padding: 20px;
|
|
968
|
+
}
|
|
969
|
+
table {
|
|
970
|
+
width: 100%;
|
|
971
|
+
border-collapse: collapse;
|
|
972
|
+
margin: 16px 0;
|
|
973
|
+
}
|
|
974
|
+
th, td {
|
|
975
|
+
padding: 12px;
|
|
976
|
+
text-align: left;
|
|
977
|
+
border-bottom: 1px solid #eee;
|
|
978
|
+
}
|
|
979
|
+
th {
|
|
980
|
+
background: #f8fafc;
|
|
981
|
+
font-weight: 600;
|
|
982
|
+
}
|
|
983
|
+
</style>
|
|
984
|
+
</head>
|
|
985
|
+
<body>
|
|
986
|
+
{% if current_user %}
|
|
987
|
+
<nav>
|
|
988
|
+
<a href="/dashboard">Dashboard</a>
|
|
989
|
+
<a href="/burrito">Burrito</a>
|
|
990
|
+
<a href="/profile">Profile</a>
|
|
991
|
+
<a href="/logout" style="float: right;">Logout ({{ current_user.email }})</a>
|
|
992
|
+
</nav>
|
|
993
|
+
{% endif %}
|
|
994
|
+
|
|
995
|
+
<div class="container">
|
|
996
|
+
{% if error %}
|
|
997
|
+
<div class="messages">
|
|
998
|
+
<div class="message error">{{ error }}</div>
|
|
999
|
+
</div>
|
|
1000
|
+
{% endif %}
|
|
1001
|
+
{% if success %}
|
|
1002
|
+
<div class="messages">
|
|
1003
|
+
<div class="message success">{{ success }}</div>
|
|
1004
|
+
</div>
|
|
1005
|
+
{% endif %}
|
|
1006
|
+
|
|
1007
|
+
{% block content %}{% endblock %}
|
|
1008
|
+
</div>
|
|
1009
|
+
|
|
1010
|
+
{% block scripts %}{% endblock %}
|
|
1011
|
+
</body>
|
|
1012
|
+
</html>
|
|
1013
|
+
|
|
1014
|
+
```
|
|
1015
|
+
|
|
1016
|
+
---
|
|
1017
|
+
|
|
1018
|
+
## app/templates/burrito.html
|
|
1019
|
+
|
|
1020
|
+
```html
|
|
1021
|
+
{% extends "base.html" %}
|
|
1022
|
+
|
|
1023
|
+
{% block title %}Burrito - Amplitude FastAPI Example{% endblock %}
|
|
1024
|
+
|
|
1025
|
+
{% block content %}
|
|
1026
|
+
<div class="card">
|
|
1027
|
+
<h1>Burrito Consideration Tracker</h1>
|
|
1028
|
+
<p>This page demonstrates custom event tracking with Amplitude.</p>
|
|
1029
|
+
|
|
1030
|
+
<div class="count" id="burrito-count">{{ burrito_count }}</div>
|
|
1031
|
+
<p style="text-align: center; color: #666;">Times you've considered a burrito</p>
|
|
1032
|
+
|
|
1033
|
+
<div style="text-align: center; margin-top: 20px;">
|
|
1034
|
+
<button onclick="considerBurrito()">Consider a Burrito</button>
|
|
1035
|
+
</div>
|
|
1036
|
+
</div>
|
|
1037
|
+
|
|
1038
|
+
<div class="card">
|
|
1039
|
+
<h3>Code Example</h3>
|
|
1040
|
+
<pre>
|
|
1041
|
+
# API endpoint captures the event
|
|
1042
|
+
client = get_amplitude_client()
|
|
1043
|
+
if client:
|
|
1044
|
+
client.track(BaseEvent(
|
|
1045
|
+
event_type='Burrito Considered',
|
|
1046
|
+
user_id=current_user.email,
|
|
1047
|
+
event_properties={'total_considerations': new_count},
|
|
1048
|
+
))</pre>
|
|
1049
|
+
</div>
|
|
1050
|
+
{% endblock %}
|
|
1051
|
+
|
|
1052
|
+
{% block scripts %}
|
|
1053
|
+
<script>
|
|
1054
|
+
async function considerBurrito() {
|
|
1055
|
+
try {
|
|
1056
|
+
const response = await fetch('/api/burrito/consider', {
|
|
1057
|
+
method: 'POST',
|
|
1058
|
+
headers: {
|
|
1059
|
+
'Content-Type': 'application/json'
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
const data = await response.json();
|
|
1063
|
+
if (data.success) {
|
|
1064
|
+
document.getElementById('burrito-count').textContent = data.count;
|
|
1065
|
+
}
|
|
1066
|
+
} catch (error) {
|
|
1067
|
+
console.error('Error:', error);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
</script>
|
|
1071
|
+
{% endblock %}
|
|
1072
|
+
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
---
|
|
1076
|
+
|
|
1077
|
+
## app/templates/dashboard.html
|
|
1078
|
+
|
|
1079
|
+
```html
|
|
1080
|
+
{% extends "base.html" %}
|
|
1081
|
+
|
|
1082
|
+
{% block title %}Dashboard - Amplitude FastAPI Example{% endblock %}
|
|
1083
|
+
|
|
1084
|
+
{% block content %}
|
|
1085
|
+
<div class="card">
|
|
1086
|
+
<h1>Dashboard</h1>
|
|
1087
|
+
<p>Welcome back, {{ current_user.email }}!</p>
|
|
1088
|
+
</div>
|
|
1089
|
+
|
|
1090
|
+
<div class="card">
|
|
1091
|
+
<h2>Amplitude Event Tracking</h2>
|
|
1092
|
+
<p>This page is tracked with Amplitude on every visit.</p>
|
|
1093
|
+
|
|
1094
|
+
<h3 style="margin-top: 20px;">Code Example</h3>
|
|
1095
|
+
<pre>
|
|
1096
|
+
# Track dashboard view
|
|
1097
|
+
client = get_amplitude_client()
|
|
1098
|
+
if client:
|
|
1099
|
+
client.track(BaseEvent(
|
|
1100
|
+
event_type='Dashboard Viewed',
|
|
1101
|
+
user_id=current_user.email,
|
|
1102
|
+
event_properties={'is_staff': current_user.is_staff},
|
|
1103
|
+
))
|
|
1104
|
+
|
|
1105
|
+
# TODO: Use Amplitude Experiment for feature flags</pre>
|
|
1106
|
+
</div>
|
|
1107
|
+
{% endblock %}
|
|
1108
|
+
|
|
1109
|
+
```
|
|
1110
|
+
|
|
1111
|
+
---
|
|
1112
|
+
|
|
1113
|
+
## app/templates/errors/404.html
|
|
1114
|
+
|
|
1115
|
+
```html
|
|
1116
|
+
{% extends "base.html" %}
|
|
1117
|
+
|
|
1118
|
+
{% block title %}Page Not Found - Amplitude FastAPI Example{% endblock %}
|
|
1119
|
+
|
|
1120
|
+
{% block content %}
|
|
1121
|
+
<div class="card">
|
|
1122
|
+
<h1>404 - Page Not Found</h1>
|
|
1123
|
+
<p>The page you're looking for doesn't exist.</p>
|
|
1124
|
+
<a href="/" class="btn">Go Home</a>
|
|
1125
|
+
</div>
|
|
1126
|
+
{% endblock %}
|
|
1127
|
+
|
|
1128
|
+
```
|
|
1129
|
+
|
|
1130
|
+
---
|
|
1131
|
+
|
|
1132
|
+
## app/templates/errors/500.html
|
|
1133
|
+
|
|
1134
|
+
```html
|
|
1135
|
+
{% extends "base.html" %}
|
|
1136
|
+
|
|
1137
|
+
{% block title %}Server Error - Amplitude FastAPI Example{% endblock %}
|
|
1138
|
+
|
|
1139
|
+
{% block content %}
|
|
1140
|
+
<div class="card">
|
|
1141
|
+
<h1>500 - Internal Server Error</h1>
|
|
1142
|
+
<p>Something went wrong on our end. Please try again later.</p>
|
|
1143
|
+
<a href="/" class="btn">Go Home</a>
|
|
1144
|
+
</div>
|
|
1145
|
+
{% endblock %}
|
|
1146
|
+
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
---
|
|
1150
|
+
|
|
1151
|
+
## app/templates/home.html
|
|
1152
|
+
|
|
1153
|
+
```html
|
|
1154
|
+
{% extends "base.html" %}
|
|
1155
|
+
|
|
1156
|
+
{% block title %}Login - Amplitude FastAPI Example{% endblock %}
|
|
1157
|
+
|
|
1158
|
+
{% block content %}
|
|
1159
|
+
<div class="card">
|
|
1160
|
+
<h1>Welcome to Amplitude FastAPI Example</h1>
|
|
1161
|
+
<p>This example demonstrates how to integrate Amplitude with a FastAPI application.</p>
|
|
1162
|
+
|
|
1163
|
+
<form method="POST">
|
|
1164
|
+
<label for="email">Email</label>
|
|
1165
|
+
<input type="email" id="email" name="email" required>
|
|
1166
|
+
|
|
1167
|
+
<label for="password">Password</label>
|
|
1168
|
+
<input type="password" id="password" name="password" required>
|
|
1169
|
+
|
|
1170
|
+
<button type="submit">Login</button>
|
|
1171
|
+
</form>
|
|
1172
|
+
|
|
1173
|
+
<p style="margin-top: 16px; font-size: 14px; color: #666;">
|
|
1174
|
+
Don't have an account? <a href="/signup">Sign up here</a>
|
|
1175
|
+
</p>
|
|
1176
|
+
<p style="font-size: 14px; color: #666;">
|
|
1177
|
+
<strong>Tip:</strong> Default credentials are admin@example.com/admin
|
|
1178
|
+
</p>
|
|
1179
|
+
</div>
|
|
1180
|
+
|
|
1181
|
+
<div class="card">
|
|
1182
|
+
<h2>Features Demonstrated</h2>
|
|
1183
|
+
<ul style="margin-left: 20px; color: #666;">
|
|
1184
|
+
<li>User registration and identification</li>
|
|
1185
|
+
<li>Event tracking</li>
|
|
1186
|
+
<li>User properties</li>
|
|
1187
|
+
</ul>
|
|
1188
|
+
</div>
|
|
1189
|
+
{% endblock %}
|
|
1190
|
+
|
|
1191
|
+
```
|
|
1192
|
+
|
|
1193
|
+
---
|
|
1194
|
+
|
|
1195
|
+
## app/templates/profile.html
|
|
1196
|
+
|
|
1197
|
+
```html
|
|
1198
|
+
{% extends "base.html" %}
|
|
1199
|
+
|
|
1200
|
+
{% block title %}Profile - Amplitude FastAPI Example{% endblock %}
|
|
1201
|
+
|
|
1202
|
+
{% block content %}
|
|
1203
|
+
<div class="card">
|
|
1204
|
+
<h1>Your Profile</h1>
|
|
1205
|
+
<p>This page demonstrates profile updates and report generation with Amplitude.</p>
|
|
1206
|
+
|
|
1207
|
+
{% if success %}
|
|
1208
|
+
<div class="message success">{{ success }}</div>
|
|
1209
|
+
{% endif %}
|
|
1210
|
+
|
|
1211
|
+
<form method="POST" action="/profile">
|
|
1212
|
+
<table>
|
|
1213
|
+
<tr>
|
|
1214
|
+
<th>Email</th>
|
|
1215
|
+
<td>{{ current_user.email }}</td>
|
|
1216
|
+
</tr>
|
|
1217
|
+
<tr>
|
|
1218
|
+
<th>Name</th>
|
|
1219
|
+
<td>
|
|
1220
|
+
<input type="text" name="name" value="{{ current_user.name or '' }}" placeholder="Enter your name">
|
|
1221
|
+
</td>
|
|
1222
|
+
</tr>
|
|
1223
|
+
<tr>
|
|
1224
|
+
<th>Date Joined</th>
|
|
1225
|
+
<td>{{ current_user.date_joined.strftime('%Y-%m-%d %H:%M') }}</td>
|
|
1226
|
+
</tr>
|
|
1227
|
+
<tr>
|
|
1228
|
+
<th>Login Count</th>
|
|
1229
|
+
<td>{{ current_user.login_count }}</td>
|
|
1230
|
+
</tr>
|
|
1231
|
+
<tr>
|
|
1232
|
+
<th>Staff Status</th>
|
|
1233
|
+
<td>{{ 'Yes' if current_user.is_staff else 'No' }}</td>
|
|
1234
|
+
</tr>
|
|
1235
|
+
</table>
|
|
1236
|
+
<button type="submit">Update Profile</button>
|
|
1237
|
+
</form>
|
|
1238
|
+
</div>
|
|
1239
|
+
|
|
1240
|
+
<div class="card">
|
|
1241
|
+
<h2>Activity Reports</h2>
|
|
1242
|
+
<p>Generate a report of your account activity:</p>
|
|
1243
|
+
|
|
1244
|
+
<div style="margin: 20px 0;">
|
|
1245
|
+
<button onclick="generateReport('summary')">Summary Report</button>
|
|
1246
|
+
<button onclick="generateReport('detailed')">Detailed Report</button>
|
|
1247
|
+
</div>
|
|
1248
|
+
|
|
1249
|
+
<div id="report-result" style="display: none;" class="message"></div>
|
|
1250
|
+
</div>
|
|
1251
|
+
|
|
1252
|
+
<div class="card">
|
|
1253
|
+
<h3>Code Example</h3>
|
|
1254
|
+
<pre>
|
|
1255
|
+
# Track profile view
|
|
1256
|
+
client = get_amplitude_client()
|
|
1257
|
+
if client:
|
|
1258
|
+
client.track(BaseEvent(
|
|
1259
|
+
event_type='Profile Viewed',
|
|
1260
|
+
user_id=current_user.email,
|
|
1261
|
+
))</pre>
|
|
1262
|
+
</div>
|
|
1263
|
+
{% endblock %}
|
|
1264
|
+
|
|
1265
|
+
{% block scripts %}
|
|
1266
|
+
<script>
|
|
1267
|
+
async function generateReport(reportType) {
|
|
1268
|
+
const resultDiv = document.getElementById('report-result');
|
|
1269
|
+
try {
|
|
1270
|
+
const formData = new FormData();
|
|
1271
|
+
formData.append('report_type', reportType);
|
|
1272
|
+
|
|
1273
|
+
const response = await fetch('/api/reports/activity', {
|
|
1274
|
+
method: 'POST',
|
|
1275
|
+
body: formData
|
|
1276
|
+
});
|
|
1277
|
+
const data = await response.json();
|
|
1278
|
+
|
|
1279
|
+
resultDiv.style.display = 'block';
|
|
1280
|
+
resultDiv.className = 'message success';
|
|
1281
|
+
resultDiv.innerHTML = '<strong>' + data.report_type + ' report generated</strong> (' + data.row_count + ' rows)<br><pre>' + JSON.stringify(data.data, null, 2) + '</pre>';
|
|
1282
|
+
} catch (error) {
|
|
1283
|
+
console.error('Error:', error);
|
|
1284
|
+
resultDiv.style.display = 'block';
|
|
1285
|
+
resultDiv.className = 'message error';
|
|
1286
|
+
resultDiv.textContent = 'Request failed: ' + error.message;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
</script>
|
|
1290
|
+
{% endblock %}
|
|
1291
|
+
|
|
1292
|
+
```
|
|
1293
|
+
|
|
1294
|
+
---
|
|
1295
|
+
|
|
1296
|
+
## app/templates/signup.html
|
|
1297
|
+
|
|
1298
|
+
```html
|
|
1299
|
+
{% extends "base.html" %}
|
|
1300
|
+
|
|
1301
|
+
{% block title %}Sign Up - Amplitude FastAPI Example{% endblock %}
|
|
1302
|
+
|
|
1303
|
+
{% block content %}
|
|
1304
|
+
<div class="card">
|
|
1305
|
+
<h1>Create an Account</h1>
|
|
1306
|
+
<p>Sign up to explore the Amplitude FastAPI integration example.</p>
|
|
1307
|
+
|
|
1308
|
+
<form method="POST">
|
|
1309
|
+
<label for="email">Email *</label>
|
|
1310
|
+
<input type="email" id="email" name="email" required>
|
|
1311
|
+
|
|
1312
|
+
<label for="password">Password *</label>
|
|
1313
|
+
<input type="password" id="password" name="password" required>
|
|
1314
|
+
|
|
1315
|
+
<label for="password_confirm">Confirm Password *</label>
|
|
1316
|
+
<input type="password" id="password_confirm" name="password_confirm" required>
|
|
1317
|
+
|
|
1318
|
+
<button type="submit">Sign Up</button>
|
|
1319
|
+
</form>
|
|
1320
|
+
|
|
1321
|
+
<p style="margin-top: 16px; font-size: 14px; color: #666;">
|
|
1322
|
+
Already have an account? <a href="/">Login here</a>
|
|
1323
|
+
</p>
|
|
1324
|
+
</div>
|
|
1325
|
+
|
|
1326
|
+
<div class="card">
|
|
1327
|
+
<h2>Amplitude Integration</h2>
|
|
1328
|
+
<p>When you sign up, the following Amplitude events are captured:</p>
|
|
1329
|
+
<ul style="margin-left: 20px; color: #666;">
|
|
1330
|
+
<li><code>client.identify()</code> - Sets user properties (email, is_staff, date_joined)</li>
|
|
1331
|
+
<li><code>User Signed Up</code> event - Tracks the signup action</li>
|
|
1332
|
+
</ul>
|
|
1333
|
+
|
|
1334
|
+
<h3 style="margin-top: 20px;">Code Example</h3>
|
|
1335
|
+
<pre>
|
|
1336
|
+
# After creating the user
|
|
1337
|
+
client = get_amplitude_client()
|
|
1338
|
+
if client:
|
|
1339
|
+
identify_obj = Identify()
|
|
1340
|
+
identify_obj.set('email', user.email)
|
|
1341
|
+
identify_obj.set('is_staff', user.is_staff)
|
|
1342
|
+
identify_obj.set('date_joined', user.date_joined.isoformat())
|
|
1343
|
+
client.identify(identify_obj, {'user_id': user.email})
|
|
1344
|
+
|
|
1345
|
+
client.track(BaseEvent(
|
|
1346
|
+
event_type='User Signed Up',
|
|
1347
|
+
user_id=user.email,
|
|
1348
|
+
event_properties={'signup_method': 'form'},
|
|
1349
|
+
))</pre>
|
|
1350
|
+
</div>
|
|
1351
|
+
{% endblock %}
|
|
1352
|
+
|
|
1353
|
+
```
|
|
1354
|
+
|
|
1355
|
+
---
|
|
1356
|
+
|
|
1357
|
+
## requirements.txt
|
|
1358
|
+
|
|
1359
|
+
```txt
|
|
1360
|
+
fastapi>=0.109.0
|
|
1361
|
+
uvicorn>=0.27.0
|
|
1362
|
+
sqlalchemy>=2.0.0
|
|
1363
|
+
python-dotenv>=1.0.0
|
|
1364
|
+
amplitude-analytics>=1.0.0
|
|
1365
|
+
pydantic>=2.0.0
|
|
1366
|
+
pydantic-settings>=2.0.0
|
|
1367
|
+
jinja2>=3.0.0
|
|
1368
|
+
python-multipart>=0.0.9
|
|
1369
|
+
werkzeug>=3.0.0
|
|
1370
|
+
itsdangerous>=2.0.0
|
|
1371
|
+
|
|
1372
|
+
```
|
|
1373
|
+
|
|
1374
|
+
---
|
|
1375
|
+
|
|
1376
|
+
## run.py
|
|
1377
|
+
|
|
1378
|
+
```py
|
|
1379
|
+
"""Development server entry point."""
|
|
1380
|
+
|
|
1381
|
+
import uvicorn
|
|
1382
|
+
|
|
1383
|
+
if __name__ == "__main__":
|
|
1384
|
+
uvicorn.run("app.main:app", host="0.0.0.0", port=5002, reload=True)
|
|
1385
|
+
|
|
1386
|
+
```
|
|
1387
|
+
|
|
1388
|
+
---
|
|
1389
|
+
|