From 2c4114c7709d3a8fdc9921307ec99c926e54c32d Mon Sep 17 00:00:00 2001 From: "wizard-ci-bot[bot]" <254716194+wizard-ci-bot[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:16:17 +0000 Subject: [PATCH] wizard-ci: rails/fizzy --- .../integration-ruby-on-rails/.posthog-wizard | 0 .../skills/integration-ruby-on-rails/SKILL.md | 68 + .../references/1-begin.md | 56 + .../references/2-edit.md | 36 + .../references/3-revise.md | 22 + .../references/4-conclude.md | 57 + .../references/EXAMPLE.md | 1320 +++++++++++++++++ .../references/identify-users.md | 272 ++++ .../references/ruby-on-rails.md | 604 ++++++++ .../references/ruby.md | 759 ++++++++++ apps/basic-integration/rails/fizzy/.gitignore | 1 + apps/basic-integration/rails/fizzy/Gemfile | 4 + .../account/cancellations_controller.rb | 2 + .../app/controllers/application_controller.rb | 6 + .../controllers/boards/columns_controller.rb | 2 + .../boards/publications_controller.rb | 4 + .../app/controllers/boards_controller.rb | 4 + .../controllers/cards/closures_controller.rb | 4 + .../controllers/cards/comments_controller.rb | 2 + .../cards/goldnesses_controller.rb | 2 + .../controllers/cards/not_nows_controller.rb | 2 + .../controllers/cards/publishes_controller.rb | 2 + .../fizzy/app/controllers/cards_controller.rb | 1 + .../sessions/magic_links_controller.rb | 4 + .../app/controllers/sessions_controller.rb | 4 + .../signups/completions_controller.rb | 5 + .../rails/fizzy/app/models/user.rb | 8 + .../app/views/layouts/shared/_head.html.erb | 11 + .../fizzy/config/initializers/posthog.rb | 15 + .../rails/fizzy/posthog-setup-report.md | 45 + 30 files changed, 3322 insertions(+) create mode 100644 apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/.posthog-wizard create mode 100644 apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/SKILL.md create mode 100644 apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/1-begin.md create mode 100644 apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/2-edit.md create mode 100644 apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/3-revise.md create mode 100644 apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/4-conclude.md create mode 100644 apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/EXAMPLE.md create mode 100644 apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/identify-users.md create mode 100644 apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/ruby-on-rails.md create mode 100644 apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/ruby.md create mode 100644 apps/basic-integration/rails/fizzy/config/initializers/posthog.rb create mode 100644 apps/basic-integration/rails/fizzy/posthog-setup-report.md diff --git a/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/.posthog-wizard b/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/.posthog-wizard new file mode 100644 index 00000000..e69de29b diff --git a/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/SKILL.md b/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/SKILL.md new file mode 100644 index 00000000..e14474d2 --- /dev/null +++ b/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/SKILL.md @@ -0,0 +1,68 @@ +--- +name: integration-ruby-on-rails +description: PostHog integration for Ruby on Rails applications +metadata: + author: PostHog + version: dev +--- + +# PostHog integration for Ruby on Rails + +This skill helps you add PostHog analytics to Ruby on Rails applications. + +## Workflow + +Follow these steps in order to complete the integration: + +1. `references/1-begin.md` - PostHog Setup - Begin ← **Start here** +2. `references/2-edit.md` - PostHog Setup - Edit +3. `references/3-revise.md` - PostHog Setup - Revise +4. `references/4-conclude.md` - PostHog Setup - Conclusion + +## Reference files + +- `references/EXAMPLE.md` - Ruby on Rails example project code +- `references/1-begin.md` - Start the event tracking setup process by analyzing the project and creating an event tracking plan +- `references/2-edit.md` - Implement PostHog event tracking in the identified files, following best practices and the example project +- `references/3-revise.md` - Review and fix any errors in the PostHog integration implementation +- `references/4-conclude.md` - Review and fix any errors in the PostHog integration implementation +- `references/ruby-on-rails.md` - Ruby on rails - docs +- `references/ruby.md` - Ruby - docs +- `references/identify-users.md` - Identify users - docs + +The example project shows the target implementation pattern. Consult the documentation for API details. + +## Key principles + +- **Environment variables**: Always use environment variables for PostHog keys. Never hardcode them. +- **Minimal changes**: Add PostHog code alongside existing integrations. Don't replace or restructure existing code. +- **Match the example**: Your implementation should follow the example project's patterns as closely as possible. + +## Framework guidelines + +- Use posthog-rails gem alongside posthog-ruby for automatic exception capture and ActiveJob instrumentation +- Run `rails generate posthog:install` to create the initializer, or manually create config/initializers/posthog.rb +- Configure auto_capture_exceptions: true to automatically track unhandled exceptions in controllers +- Configure report_rescued_exceptions: true to also capture exceptions that Rails rescues (e.g. with rescue_from) +- Configure auto_instrument_active_job: true to track background job failures with job class, queue, and arguments +- Use PostHog.capture() and PostHog.identify() class-level methods (NOT instance methods) — the posthog-rails gem manages the client lifecycle via PostHog.init +- Do NOT manually create PostHog::Client instances in Rails — use PostHog.init in the initializer and PostHog.capture/identify everywhere else +- capture_exception takes POSITIONAL args: PostHog.capture_exception(exception, distinct_id, additional_properties) — do NOT use keyword args +- Define posthog_distinct_id on the User model for automatic user association in error reports — posthog-rails auto-detects by trying: posthog_distinct_id, distinct_id, id, pk, uuid (in order) +- For ActiveJob user association, use the class-level DSL `posthog_distinct_id ->(user) { user.email }` or pass user_id: in a hash argument +- Store API key in Rails credentials or environment variables, never hardcode +- For frontend tracking alongside posthog-rails, add the posthog-js snippet to the layout template — posthog-js handles pageviews, session replay, and client-side errors while posthog-ruby handles backend events, server errors, feature flags, and background jobs +- posthog-ruby is the Ruby SDK gem name (add `gem 'posthog-ruby'` to Gemfile) but require it with `require 'posthog'` (NOT `require 'posthog-ruby'`) +- Use PostHog::Client.new(api_key: key, host: host) for instance-based initialization in scripts and CLIs +- In CLIs and scripts: MUST call client.shutdown before exit or all events are lost +- Use begin/rescue/ensure with shutdown in the ensure block for proper cleanup +- capture and identify take a single hash argument: client.capture(distinct_id: 'user_123', event: 'my_event', properties: { key: 'value' }) +- capture_exception takes POSITIONAL args (not keyword): client.capture_exception(exception, distinct_id, additional_properties) — do NOT use `distinct_id:` keyword syntax + +## Identifying users + +Identify users during login and signup events. Refer to the example code and documentation for the correct identify pattern for this framework. If both frontend and backend code exist, pass the client-side session and distinct ID using `X-POSTHOG-DISTINCT-ID` and `X-POSTHOG-SESSION-ID` headers to maintain correlation. + +## Error tracking + +Add PostHog error tracking to relevant files, particularly around critical user flows and API boundaries. diff --git a/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/1-begin.md b/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/1-begin.md new file mode 100644 index 00000000..55f0a832 --- /dev/null +++ b/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/1-begin.md @@ -0,0 +1,56 @@ +--- +title: PostHog Setup - Begin +description: Start the event tracking setup process by analyzing the project and creating an event tracking plan +--- + +We're making an event tracking plan for this project. + +This is the first of several phases — plan the events, implement them, revise and validate changes, then conclude by creating a dashboard and writing a setup report. + +## Task list + +As soon as you've read this description and have a rough sense of the work, make a single **call `TaskCreate` immediately** before reading any reference file or beginning analysis. The user is watching the task pane and shouldn't see it sit empty. + +It's fine if your first list is incomplete or imprecise. Seed it with whatever high-level items you can infer from the overview above, then call `TaskCreate` again (or `TaskUpdate` to refine existing items) every time your understanding sharpens: after a phase reveals work you didn't anticipate, after planning surfaces concrete sub-items, after you hit something new. Use `TaskUpdate` to mark items `in_progress` when you start them and `completed` when you finish. Keeping the list current matters more than getting it right on the first call. + +Keep task titles broad and job-oriented. Describe the purpose or area of work with wording like "Planning event tracking", "Identifying users", "Installing PostHog", "Capturing events", or "Creating dashboards", not the specific files, paths, or symbols involved. Adjust the task names according to the user's project and context. + +Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. + +From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. + +Look for opportunities to track client-side events. + +**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: + + - Payment/checkout completion + - Webhook handlers + - Authentication endpoints + +Do not skip server-side events - they capture actions that cannot be tracked client-side. + +Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add with these exact field names: `event_name` (the event name), `event_description` (one sentence), and `file` (the file path the event goes in). The wizard reads this file to surface the plan in the UI. If events already exist, don't duplicate them; supplement them. + +Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. + +As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. + +## Status + +Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: + +[STATUS] Checking project structure. + +Status to report in this phase: + +- Checking project structure +- Verifying PostHog dependencies +- Generating events based on project + +## Abort statuses + +If and only if the instructions have `[ABORT]` states specified, and you clearly match the conditions for an abort, emit the abort message. Do NOT attempt to exit or halt yourself — the wizard's middleware catches `[ABORT]` and terminates the run for you. + +--- + +**Upon completion, continue with:** [2-edit.md](2-edit.md) \ No newline at end of file diff --git a/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/2-edit.md b/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/2-edit.md new file mode 100644 index 00000000..e5f7ffd1 --- /dev/null +++ b/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/2-edit.md @@ -0,0 +1,36 @@ +--- +title: PostHog Setup - Edit +description: Implement PostHog event tracking in the identified files, following best practices and the example project +--- + +For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. + +Use environment variables for PostHog keys. Do not hardcode PostHog keys. + +If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. + +For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. + +Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. + +Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. + +It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. + +You should also add PostHog exception capture error tracking to these files where relevant. + +Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. + +Remember the documentation and example project resources you were provided at the beginning. Read them now. + +## Status + +Status to report in this phase: + +- Inserting PostHog capture code +- A status message for each file whose edits you are planning, including a high level summary of changes +- A status message for each file you have edited + +--- + +**Upon completion, continue with:** [3-revise.md](3-revise.md) \ No newline at end of file diff --git a/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/3-revise.md b/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/3-revise.md new file mode 100644 index 00000000..3b07f506 --- /dev/null +++ b/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/3-revise.md @@ -0,0 +1,22 @@ +--- +title: PostHog Setup - Revise +description: Review and fix any errors in the PostHog integration implementation +--- + +Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. + +Ensure that any components created were actually used. + +Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. + +## Status + +Status to report in this phase: + +- Finding and correcting errors +- Report details of any errors you fix +- Linting, building and prettying + +--- + +**Upon completion, continue with:** [4-conclude.md](4-conclude.md) \ No newline at end of file diff --git a/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/4-conclude.md b/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/4-conclude.md new file mode 100644 index 00000000..d876d435 --- /dev/null +++ b/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/4-conclude.md @@ -0,0 +1,57 @@ +--- +title: PostHog Setup - Conclusion +description: Review and fix any errors in the PostHog integration implementation +--- + +Use the PostHog MCP to create a new dashboard named "Analytics basics (wizard)" based on the events created here. Keep the `(wizard)` tag with that exact casing so anyone browsing PostHog can see the wizard created this dashboard, and so a quick search for `(wizard)` surfaces every wizard-created artifact in one go. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. + +Once the dashboard exists, emit its URL on its own line in your assistant message using this exact marker: `[DASHBOARD_URL] `. The wizard parses this marker from your visible message and surfaces the link in the success summary. Mentioning the URL only in thinking or in prose without the marker means the link is dropped. + +Search for a file called `.posthog-events.json` and read it for available events. + +Do not spawn subagents. + +Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, a list of links for the dashboard and insights created, and a "Verify before merging" checklist (see below). Follow this format: + + +# PostHog post-wizard report + +The wizard has completed a deep integration of your project. [Detailed summary of changes] + +[table of events/descriptions/files] + +## Next steps + +We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: + +[links] + +## Verify before merging + +[checklist] + +### Agent skill + +We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. + + + +For the "Verify before merging" checklist, write GitHub-style checkboxes (`- [ ] ...`) covering what the developer (or their coding agent) still needs to do to take this from "wizard finished" to "merged". Include ONLY the items that actually apply to the integration you just performed — judge each against the code you changed in this run, and drop any that don't fit. Phrase each item as a concrete, checkable action. Candidate items, with the condition for including each: + +- Always: "Run a full production build (the wizard only verified the files it touched) and fix any lint or type errors introduced by the generated code." +- Always: "Run the test suite — call sites that were rewritten or instrumented may need updated mocks or fixtures." +- If you added environment variables: "Add the exact PostHog env var names you added to `.env.example` and any monorepo/bootstrap scripts so collaborators know what to set." +- If this integration ships a minified production browser bundle (most SPA/SSR web frameworks — e.g. Next.js, Nuxt, SvelteKit, Astro, Vite-based apps): "Wire source-map upload (`posthog-cli sourcemap` or your bundler's upload step) into CI so production stack traces de-minify." +- If LLM analytics was set up in this run: "Trigger the LLM call path(s) you instrumented and confirm `$ai_generation` events appear in PostHog AI Observability." +- If the app has user auth and an `identify` call was added: "Confirm the returning-visitor path also calls `identify` — a handler that only identifies on fresh login can leave returning sessions on anonymous distinct IDs." + +Do not invent items beyond what applies. If only the two "Always" items apply, the checklist is just those two. + +Upon completion, remove .posthog-events.json. + +## Status + +Status to report in this phase: + +- Configured dashboard: [insert PostHog dashboard URL] +- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/EXAMPLE.md b/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/EXAMPLE.md new file mode 100644 index 00000000..99032096 --- /dev/null +++ b/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/EXAMPLE.md @@ -0,0 +1,1320 @@ +# PostHog Ruby on Rails Example Project + +Repository: https://github.com/PostHog/context-mill +Path: example-apps/ruby-on-rails + +--- + +## README.md + +# PostHog Ruby on Rails example + +This is a [Ruby on Rails](https://rubyonrails.org) example demonstrating PostHog integration with product analytics, error tracking (auto-instrumentation), feature flags, user identification, and ActiveJob instrumentation via the `posthog-rails` gem. + +## Features + +- **Product analytics**: Track user events and behaviors with `PostHog.capture` +- **Error tracking (auto)**: Unhandled exceptions captured automatically by `posthog-rails` +- **Error tracking (manual)**: Handled errors captured with `PostHog.capture_exception` +- **Rails.error integration**: Rails 7+ error reporting captured automatically +- **ActiveJob instrumentation**: Background job failures captured automatically +- **User identification**: Associate events with authenticated users via `PostHog.identify` +- **Feature flags**: Control feature rollouts with `PostHog.is_feature_enabled` +- **User context**: Exceptions automatically associated with `current_user` +- **Frontend tracking**: posthog-js captures pageviews and session replay alongside backend events + +## Getting started + +### 1. Install dependencies + +```bash +bundle install +``` + +### 2. Configure environment variables + +```bash +cp .env.example .env +# Edit .env and add your PostHog project token +``` + +Get your PostHog project token from your [PostHog project settings](https://app.posthog.com/project/settings). + +### 3. Setup database + +```bash +bin/rails db:create db:migrate db:seed +``` + +### 4. Run the development server + +```bash +bin/rails server +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser. Login with `admin@example.com` / `admin`. + +## Project structure + +``` +ruby-on-rails/ +├── config/ +│ ├── routes.rb # URL routing +│ └── initializers/ +│ └── posthog.rb # PostHog + posthog-rails configuration +├── app/ +│ ├── controllers/ +│ │ ├── application_controller.rb # Base controller with current_user +│ │ ├── sessions_controller.rb # Login/logout with PostHog identify +│ │ ├── registrations_controller.rb # Signup with PostHog identify +│ │ ├── dashboard_controller.rb # Feature flags + ActiveJob demo +│ │ ├── burritos_controller.rb # Custom event tracking +│ │ ├── profiles_controller.rb # Page view tracking +│ │ └── errors_controller.rb # Error tracking demos +│ ├── jobs/ +│ │ └── example_job.rb # ActiveJob auto-instrumentation demo +│ ├── models/ +│ │ └── user.rb # posthog_distinct_id + posthog_properties +│ └── views/ +│ ├── layouts/application.html.erb # Base layout with posthog-js snippet +│ ├── sessions/new.html.erb # Login page +│ ├── registrations/new.html.erb # Signup page +│ ├── dashboard/show.html.erb # Feature flags demo +│ ├── burritos/show.html.erb # Event tracking demo +│ └── profiles/show.html.erb # Error tracking demo +├── db/ +│ ├── migrate/ # Database migrations +│ └── seeds.rb # Default admin user +├── .env.example # Environment variable template +├── Gemfile # Ruby dependencies +└── README.md # This file +``` + +## Key integration points + +### PostHog initialization (config/initializers/posthog.rb) + +```ruby +# Rails-specific auto-instrumentation +PostHog::Rails.configure do |config| + config.auto_capture_exceptions = true + config.report_rescued_exceptions = true + config.auto_instrument_active_job = true + config.capture_user_context = true + config.current_user_method = :current_user + config.user_id_method = :posthog_distinct_id +end + +PostHog.init do |config| + config.api_key = ENV.fetch('POSTHOG_PROJECT_TOKEN', nil) + config.host = ENV.fetch('POSTHOG_HOST', 'https://us.i.posthog.com') +end +``` + +### User model (app/models/user.rb) + +```ruby +class User < ApplicationRecord + has_secure_password + + # Called by posthog-rails for automatic user association in error reports + def posthog_distinct_id + email + end + + def posthog_properties + { email: email, is_staff: is_staff, date_joined: created_at&.iso8601 } + end +end +``` + +### User identification (app/controllers/sessions_controller.rb) + +```ruby +# Identify the user and capture login event +PostHog.identify( + distinct_id: user.posthog_distinct_id, + properties: user.posthog_properties +) + +PostHog.capture( + distinct_id: user.posthog_distinct_id, + event: 'user_logged_in', + properties: { login_method: 'email' } +) +``` + +### Feature flags (app/controllers/dashboard_controller.rb) + +```ruby +# Check if a feature flag is enabled +@show_new_feature = PostHog.is_feature_enabled( + 'new-dashboard-feature', + user.posthog_distinct_id, + person_properties: user.posthog_properties +) + +# Get feature flag payload for configuration +@feature_config = PostHog.get_feature_flag_payload( + 'new-dashboard-feature', + user.posthog_distinct_id +) +``` + +### Error tracking — auto-capture + +With `auto_capture_exceptions: true`, unhandled exceptions in controllers are captured automatically. No code needed: + +```ruby +# This exception is automatically captured by posthog-rails +# with the current_user's posthog_distinct_id attached +def show + raise "Something went wrong" # Captured automatically! +end +``` + +### Error tracking — manual capture + +```ruby +begin + risky_operation +rescue => e + PostHog.capture_exception(e, current_user.posthog_distinct_id) +end +``` + +### Error tracking — Rails.error integration + +```ruby +# posthog-rails subscribes to Rails.error automatically +Rails.error.handle(context: { user_id: user.id }) do + risky_operation +end +``` + +### ActiveJob instrumentation + +```ruby +# config: auto_instrument_active_job = true +# Job failures are captured automatically. +# Use the posthog_distinct_id DSL to associate errors with a user. +class ExampleJob < ApplicationJob + posthog_distinct_id ->(distinct_id, *) { distinct_id } + + def perform(distinct_id, should_fail: false) + raise "Job failed" # Captured automatically with user context + end +end + +# In the controller, pass the distinct_id when enqueuing: +ExampleJob.perform_later(current_user.posthog_distinct_id, should_fail: true) +``` + +## Frontend + Backend integration + +This example includes the posthog-js snippet in the layout template to demonstrate how frontend and backend tracking work together. + +### How it works + +1. **posthog-js** (frontend) captures pageviews, clicks, and session replay +2. **posthog-ruby + posthog-rails** (backend) captures business logic events, errors, and feature flag evaluations +3. **Shared distinct_id** — frontend and backend events are linked when the same `distinct_id` is used on both sides. Call `posthog.identify(user.email)` in posthog-js after login, matching the `posthog_distinct_id` used on the backend +4. **Session replay** lets you watch user sessions where errors occurred + +**Note:** Unlike the Django SDK, posthog-rails does not include a context middleware that reads `X-POSTHOG-SESSION-ID` or `X-POSTHOG-DISTINCT-ID` tracing headers. Frontend and backend events are correlated through the shared `distinct_id`. + +### When to track frontend vs backend + +- **Frontend**: UI interactions, client-side errors, session replay, pageviews +- **Backend**: Business logic (signups, purchases), server errors, feature flag evaluations, background jobs + +## Learn more + +- [PostHog Ruby on Rails integration](https://posthog.com/docs/libraries/ruby-on-rails) +- [PostHog Ruby SDK](https://posthog.com/docs/libraries/ruby) +- [PostHog Error Tracking](https://posthog.com/docs/error-tracking) +- [Ruby on Rails documentation](https://guides.rubyonrails.org/) + +--- + +## .env.example + +```example +# PostHog Configuration +POSTHOG_PROJECT_TOKEN=phc_your_project_token_here +POSTHOG_HOST=https://us.i.posthog.com + +# Optional: Enable debug mode to see PostHog requests +# POSTHOG_DEBUG=true + +``` + +--- + +## app/controllers/application_controller.rb + +```rb +class ApplicationController < ActionController::Base + protect_from_forgery with: :exception + + private + + def current_user + @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id] + end + helper_method :current_user + + def require_login + unless current_user + redirect_to login_path + end + end +end + +``` + +--- + +## app/controllers/burritos_controller.rb + +```rb +class BurritosController < ApplicationController + before_action :require_login + + def show + @burrito_count = session[:burrito_count] || 0 + end + + def consider + count = (session[:burrito_count] || 0) + 1 + session[:burrito_count] = count + + user = current_user + + # PostHog: Track custom event + PostHog.identify( + distinct_id: user.posthog_distinct_id, + properties: user.posthog_properties + ) + + PostHog.capture( + distinct_id: user.posthog_distinct_id, + event: 'burrito_considered', + properties: { total_considerations: count } + ) + + render json: { success: true, count: count } + end +end + +``` + +--- + +## app/controllers/dashboard_controller.rb + +```rb +class DashboardController < ApplicationController + before_action :require_login + + def show + user = current_user + + # PostHog: Track dashboard view + PostHog.capture( + distinct_id: user.posthog_distinct_id, + event: 'dashboard_viewed', + properties: { is_staff: user.is_staff } + ) + + # PostHog: Check feature flag + @show_new_feature = PostHog.is_feature_enabled( + 'new-dashboard-feature', + user.posthog_distinct_id, + person_properties: user.posthog_properties + ) + + # PostHog: Get feature flag payload for configuration + @feature_config = PostHog.get_feature_flag_payload( + 'new-dashboard-feature', + user.posthog_distinct_id + ) + end + + def enqueue_test_job + # Enqueue a job that will fail — posthog-rails captures the error automatically. + # The distinct_id is passed so the posthog_distinct_id DSL can associate the error with this user. + ExampleJob.perform_later(current_user.posthog_distinct_id, should_fail: true) + + render json: { + success: true, + message: 'Job enqueued. The job will fail and posthog-rails will capture the error automatically.' + } + end +end + +``` + +--- + +## app/controllers/errors_controller.rb + +```rb +class ErrorsController < ApplicationController + before_action :require_login + + def test + # Manual exception capture — catch the error and report it explicitly + begin + raise StandardError, 'Test exception from critical operation' + rescue StandardError => e + # PostHog: Manually capture the exception + PostHog.capture_exception(e, current_user.posthog_distinct_id) + + PostHog.capture( + distinct_id: current_user.posthog_distinct_id, + event: 'error_triggered', + properties: { + error_type: e.class.name, + error_message: e.message + } + ) + + render json: { + success: false, + error: e.message, + message: 'Error has been captured by PostHog' + }, status: :internal_server_error + end + end + + def test_rails_error + # Rails.error.handle — Rails 7+ error reporting integration. + # posthog-rails subscribes to Rails.error, so exceptions reported + # via Rails.error.handle are automatically captured in PostHog. + Rails.error.handle(context: { user_id: current_user.id }) do + raise StandardError, 'Test error via Rails.error.handle — captured automatically by posthog-rails' + end + + render json: { + success: true, + message: 'Error was handled via Rails.error.handle and captured by posthog-rails' + } + end +end + +``` + +--- + +## app/controllers/profiles_controller.rb + +```rb +class ProfilesController < ApplicationController + before_action :require_login + + def show + # PostHog: Track profile view + PostHog.capture( + distinct_id: current_user.posthog_distinct_id, + event: 'profile_viewed' + ) + end +end + +``` + +--- + +## app/controllers/registrations_controller.rb + +```rb +class RegistrationsController < ApplicationController + def new + redirect_to dashboard_path if current_user + end + + def create + user = User.new( + email: params[:email], + password: params[:password], + password_confirmation: params[:password_confirmation] + ) + + if user.save + session[:user_id] = user.id + + # PostHog: Identify the new user and capture signup event + PostHog.identify( + distinct_id: user.posthog_distinct_id, + properties: user.posthog_properties + ) + + PostHog.capture( + distinct_id: user.posthog_distinct_id, + event: 'user_signed_up', + properties: { signup_method: 'form' } + ) + + redirect_to dashboard_path + else + flash[:error] = user.errors.full_messages.join(', ') + render :new, status: :unprocessable_entity + end + end +end + +``` + +--- + +## app/controllers/sessions_controller.rb + +```rb +class SessionsController < ApplicationController + def new + redirect_to dashboard_path if current_user + end + + def create + user = User.find_by(email: params[:email]) + + if user&.authenticate(params[:password]) + session[:user_id] = user.id + + # PostHog: Identify the user and capture login event + PostHog.identify( + distinct_id: user.posthog_distinct_id, + properties: user.posthog_properties + ) + + PostHog.capture( + distinct_id: user.posthog_distinct_id, + event: 'user_logged_in', + properties: { login_method: 'email' } + ) + + redirect_to dashboard_path + else + flash[:error] = 'Invalid email or password' + render :new, status: :unprocessable_entity + end + end + + def destroy + if current_user + # PostHog: Track logout before session ends + PostHog.capture( + distinct_id: current_user.posthog_distinct_id, + event: 'user_logged_out' + ) + end + + session.delete(:user_id) + redirect_to login_path + end +end + +``` + +--- + +## app/jobs/application_job.rb + +```rb +class ApplicationJob < ActiveJob::Base +end + +``` + +--- + +## app/jobs/example_job.rb + +```rb +# Example ActiveJob demonstrating posthog-rails auto-instrumentation. +# +# When auto_instrument_active_job is enabled in the PostHog config, +# posthog-rails automatically captures exceptions from failed jobs. +# The job class name, queue, and arguments are included as properties +# on the error event. +# +# Use the posthog_distinct_id DSL to associate job errors with a user. +# The proc receives the same arguments as perform and should return +# the distinct_id string. Without this, job errors have no user context. +class ExampleJob < ApplicationJob + queue_as :default + + # Extract distinct_id from the first argument so posthog-rails + # can associate the error with the user who triggered the job. + posthog_distinct_id ->(distinct_id, *) { distinct_id } + + def perform(distinct_id, should_fail: false) + if should_fail + raise StandardError, 'Example job failure - this error is automatically captured by posthog-rails' + end + + Rails.logger.info "ExampleJob completed successfully for #{distinct_id}" + end +end + +``` + +--- + +## app/models/application_record.rb + +```rb +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end + +``` + +--- + +## app/models/user.rb + +```rb +class User < ApplicationRecord + has_secure_password + + validates :email, presence: true, uniqueness: true + + # Called by posthog-rails for automatic user association in error reports. + # When auto_capture_exceptions and capture_user_context are enabled, + # posthog-rails calls this method on current_user to get the distinct_id. + def posthog_distinct_id + email + end + + # Helper used by controllers when calling PostHog.identify to set person properties. + # These properties appear on the person profile in PostHog. + def posthog_properties + { + email: email, + is_staff: is_staff, + date_joined: created_at&.iso8601 + } + end +end + +``` + +--- + +## app/views/burritos/show.html.erb + +```erb +<% content_for(:title) { 'Burrito - PostHog Rails example' } %> + +
+

Burrito consideration tracker

+

This page demonstrates custom event tracking with PostHog.

+
+ +
+

Times considered

+
<%= @burrito_count %>
+ +
+ +
+

How event tracking works

+

Each time you click the button, a burrito_considered event is sent to PostHog:

+
PostHog.capture(
+  distinct_id: user.posthog_distinct_id,
+  event: 'burrito_considered',
+  properties: { total_considerations: count }
+)
+
+ +<% content_for :scripts do %> + +<% end %> + +``` + +--- + +## app/views/dashboard/show.html.erb + +```erb +<% content_for(:title) { 'Dashboard - PostHog Rails example' } %> + +
+

Dashboard

+

Welcome back, <%= current_user.email %>!

+
+ +
+

Feature flags

+

Feature flags allow you to control feature rollouts and run A/B tests.

+ + <% if @show_new_feature %> +
+

New feature enabled!

+

+ This section is only visible because the new-dashboard-feature + flag is enabled for your user. +

+ <% if @feature_config %> +

Feature config: <%= @feature_config %>

+ <% end %> +
+ <% else %> +
+

+ The new-dashboard-feature flag is not enabled for your user. + Create this flag in your PostHog project to see it in action. +

+
+ <% end %> +
+ +
+

ActiveJob instrumentation

+

+ Click below to enqueue a background job that will fail. + posthog-rails automatically captures the exception — no extra code needed. +

+ + +
+ +
+

How feature flags work

+
# Check if a feature flag is enabled
+show_feature = PostHog.is_feature_enabled(
+  'new-dashboard-feature',
+  user.posthog_distinct_id,
+  person_properties: user.posthog_properties
+)
+
+# Get feature flag payload for configuration
+config = PostHog.get_feature_flag_payload(
+  'new-dashboard-feature',
+  user.posthog_distinct_id
+)
+
+ +<% content_for :scripts do %> + +<% end %> + +``` + +--- + +## app/views/layouts/application.html.erb + +```erb + + + + + + <%= content_for?(:title) ? yield(:title) : 'PostHog Rails example' %> + <%= csrf_meta_tags %> + + + + + + + <% if current_user %> + + <% end %> + +
+ <% if flash[:error] %> +
<%= flash[:error] %>
+ <% end %> + <% if flash[:notice] %> +
<%= flash[:notice] %>
+ <% end %> + + <%= yield %> +
+ + <%= yield :scripts %> + + + +``` + +--- + +## app/views/profiles/show.html.erb + +```erb +<% content_for(:title) { 'Profile - PostHog Rails example' } %> + +
+

Profile

+

This page demonstrates error tracking with PostHog and posthog-rails.

+
+ +
+

User information

+ + + + + + + + + + + + + +
Email:<%= current_user.email %>
Date Joined:<%= current_user.created_at %>
Staff Status:<%= current_user.is_staff ? 'Yes' : 'No' %>
+
+ +
+

Error tracking demo

+

Click the buttons below to trigger different types of errors and see how PostHog captures them.

+ +
+ + +
+ + +
+ +
+

How error tracking works

+

Auto-capture (no code needed):

+
# config/initializers/posthog.rb
+PostHog::Rails.configure do |config|
+  config.auto_capture_exceptions = true
+  config.capture_user_context = true
+end
+# That's it! Unhandled exceptions are captured automatically.
+ +

Manual capture:

+
begin
+  risky_operation
+rescue => e
+  PostHog.capture_exception(e, user.posthog_distinct_id)
+end
+ +

Rails.error integration:

+
# posthog-rails subscribes to Rails.error automatically
+Rails.error.handle(context: { user_id: user.id }) do
+  risky_operation
+end
+
+ +<% content_for :scripts do %> + +<% end %> + +``` + +--- + +## app/views/registrations/new.html.erb + +```erb +<% content_for(:title) { 'Sign Up - PostHog Rails example' } %> + +
+

Sign Up

+

Create an account to see PostHog analytics in action.

+ +
+ <%= hidden_field_tag :authenticity_token, form_authenticity_token %> + + + + +
+ +

+ Already have an account? Login +

+
+ +``` + +--- + +## app/views/sessions/new.html.erb + +```erb +<% content_for(:title) { 'Login - PostHog Rails example' } %> + +
+

PostHog Rails example

+

Welcome! This example demonstrates PostHog integration with Ruby on Rails, including automatic error tracking via posthog-rails.

+
+ +
+

Login

+

Login to see PostHog analytics in action.

+ +
+ <%= hidden_field_tag :authenticity_token, form_authenticity_token %> + + + +
+ +

+ Don't have an account? Sign up
+ Tip: Run bin/rails db:seed to create admin@example.com / admin +

+
+ +
+

What this example demonstrates

+ +
+ +``` + +--- + +## config.ru + +```ru +require_relative 'config/environment' +run Rails.application + +``` + +--- + +## config/application.rb + +```rb +require_relative 'boot' +require 'rails/all' + +Bundler.require(*Rails.groups) + +module PosthogExample + class Application < Rails::Application + config.load_defaults 7.1 + + # Use SQLite for all stores + config.active_job.queue_adapter = :async + end +end + +``` + +--- + +## config/boot.rb + +```rb +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +require 'bundler/setup' + +``` + +--- + +## config/environment.rb + +```rb +require_relative 'application' +Rails.application.initialize! + +``` + +--- + +## config/environments/development.rb + +```rb +require 'active_support/core_ext/integer/time' + +Rails.application.configure do + config.enable_reloading = true + config.eager_load = false + config.consider_all_requests_local = true + config.server_timing = true + + # Secret key for development (not used in production) + config.secret_key_base = 'dev-secret-key-for-posthog-example-only' + + config.action_controller.perform_caching = false + config.cache_store = :memory_store + + config.active_support.deprecation = :log + config.active_support.disallowed_deprecation = :raise + config.active_support.disallowed_deprecation_warnings = [] + + config.active_record.migration_error = :page_load + config.active_record.verbose_query_logs = true +end + +``` + +--- + +## config/initializers/posthog.rb + +```rb +# PostHog configuration with posthog-rails auto-instrumentation +# +# The posthog-rails gem provides: +# - Automatic exception capture for unhandled controller errors +# - ActiveJob instrumentation for background job failures +# - User context detection from current_user +# - Rails.error integration for rescued exceptions +PostHog.init do |config| + config.api_key = ENV.fetch('POSTHOG_PROJECT_TOKEN', nil) + config.host = ENV.fetch('POSTHOG_HOST', 'https://us.i.posthog.com') +end + +PostHog::Rails.configure do |config| + # Auto-capture unhandled exceptions in controllers + config.auto_capture_exceptions = true + + # Also capture exceptions that Rails rescues (e.g. ActiveRecord::RecordNotFound) + config.report_rescued_exceptions = true + + # Auto-instrument ActiveJob failures + config.auto_instrument_active_job = true + + # Automatically associate errors with the current user + config.capture_user_context = true + config.current_user_method = :current_user + config.user_id_method = :posthog_distinct_id +end + + +``` + +--- + +## config/routes.rb + +```rb +Rails.application.routes.draw do + # Auth + get 'login', to: 'sessions#new' + post 'login', to: 'sessions#create' + delete 'logout', to: 'sessions#destroy' + + get 'signup', to: 'registrations#new' + post 'signup', to: 'registrations#create' + + # App + get 'dashboard', to: 'dashboard#show' + get 'burrito', to: 'burritos#show' + post 'api/burrito/consider', to: 'burritos#consider' + get 'profile', to: 'profiles#show' + + # Error tracking demos + post 'api/test-error', to: 'errors#test' + post 'api/test-rails-error', to: 'errors#test_rails_error' + + # Background job demo + post 'api/test-job', to: 'dashboard#enqueue_test_job' + + root 'sessions#new' +end + +``` + +--- + +## db/migrate/20240101000000_create_users.rb + +```rb +class CreateUsers < ActiveRecord::Migration[7.1] + def change + create_table :users do |t| + t.string :email, null: false + t.string :password_digest, null: false + t.boolean :is_staff, default: false + + t.timestamps + end + + add_index :users, :email, unique: true + end +end + +``` + +--- + +## db/schema.rb + +```rb +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[7.2].define(version: 2024_01_01_000000) do + create_table "users", force: :cascade do |t| + t.string "email", null: false + t.string "password_digest", null: false + t.boolean "is_staff", default: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["email"], name: "index_users_on_email", unique: true + end +end + +``` + +--- + +## db/seeds.rb + +```rb +# Create a default admin user for testing +User.find_or_create_by!(email: 'admin@example.com') do |user| + user.password = 'admin' + user.password_confirmation = 'admin' + user.is_staff = true +end + +puts 'Seed data created: admin@example.com / admin' + +``` + +--- + +## Gemfile + +``` +source 'https://rubygems.org' + +gem 'rails', '~> 7.1' +gem 'sqlite3', '~> 1.7' +gem 'puma', '~> 6.0' +gem 'bcrypt', '~> 3.1' +gem 'dotenv-rails', '~> 3.0' + +# PostHog +gem 'posthog-ruby', '~> 3.0' +gem 'posthog-rails' + +``` + +--- + +## Rakefile + +``` +require_relative 'config/application' +Rails.application.load_tasks + +``` + +--- + diff --git a/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/identify-users.md b/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/identify-users.md new file mode 100644 index 00000000..1417e03a --- /dev/null +++ b/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/identify-users.md @@ -0,0 +1,272 @@ +# Identify users - Docs + +Linking events to specific users enables you to build a full picture of how they're using your product across different sessions, devices, and platforms. + +This is straightforward to do when [capturing backend events](/docs/product-analytics/capture-events?tab=Node.js.md), as you associate events to a specific user using a `distinct_id`, which is a required argument. + +However, in the frontend of a [web](/docs/libraries/js/features.md#capturing-events) or [mobile app](/docs/libraries/ios.md#capturing-events), a `distinct_id` is not a required argument — PostHog's SDKs will generate an anonymous `distinct_id` for you automatically and you can capture events anonymously, provided you use the appropriate [configuration](/docs/libraries/js/features.md#capturing-anonymous-events). + +To link events to specific users, call `identify`: + +PostHog AI + +### Web + +```javascript +posthog.identify( + 'distinct_id', // Replace 'distinct_id' with your user's unique identifier + { email: 'max@hedgehogmail.com', name: 'Max Hedgehog' } // optional: set additional person properties +); +``` + +### Android + +```kotlin +PostHog.identify( + distinctId = distinctID, // Replace 'distinctID' with your user's unique identifier + // optional: set additional person properties + userProperties = mapOf( + "name" to "Max Hedgehog", + "email" to "max@hedgehogmail.com" + ) +) +``` + +### iOS + +```swift +PostHogSDK.shared.identify("distinct_id", // Replace "distinct_id" with your user's unique identifier + userProperties: ["name": "Max Hedgehog", "email": "max@hedgehogmail.com"]) // optional: set additional person properties +``` + +### React Native + +```jsx +posthog.identify('distinct_id', { // Replace "distinct_id" with your user's unique identifier + email: 'max@hedgehogmail.com', // optional: set additional person properties + name: 'Max Hedgehog' +}) +``` + +### Dart + +```dart +await Posthog().identify( + userId: 'distinct_id', // Replace "distinct_id" with your user's unique identifier + userProperties: { + 'email': 'max@hedgehogmail.com', // optional: set additional person properties + 'name': 'Max Hedgehog', + }, +); +``` + +Events captured after calling `identify` are identified events and this creates a person profile if one doesn't exist already. + +Due to the cost of processing them, anonymous events can be up to 4x cheaper than identified events, so it's recommended you only capture identified events when needed. + +## How identify works + +When a user starts browsing your website or app, PostHog automatically assigns them an **anonymous ID**, which is stored locally. + +Provided you've [configured persistence](/docs/libraries/js/persistence.md) to use cookies or `localStorage`, this enables us to track anonymous users – even across different sessions. + +By calling `identify` with a `distinct_id` of your choice (usually the user's ID in your database, or their email), you link the anonymous ID and distinct ID together. + +Thus, all past and future events made with that anonymous ID are now associated with the distinct ID. + +This enables you to do things like associate events with a user from before they log in for the first time, or associate their events across different devices or platforms. + +Using identify in the backend + +Although you can call `identify` using our backend SDKs, it is used most in frontends. This is because there is no concept of anonymous sessions in the backend SDKs, so calling `identify` only updates person profiles. + +## Best practices when using `identify` + +### 1\. Call `identify` as soon as you're able to + +In your frontend, you should call `identify` as soon as you're able to. + +Typically, this is every time your **app loads** for the first time, and directly after your **users log in**. + +This ensures that events sent during your users' sessions are correctly associated with them. + +You only need to call `identify` once per session, and you should avoid calling it multiple times unnecessarily. + +If you call `identify` multiple times with the same data without reloading the page in between, PostHog will ignore the subsequent calls. + +### 2\. Use unique strings for distinct IDs + +If two users have the same distinct ID, their data is merged and they are considered one user in PostHog. Two common ways this can happen are: + +- Your logic for generating IDs does not generate sufficiently strong IDs and you can end up with a clash where 2 users have the same ID. +- There's a bug, typo, or mistake in your code leading to most or all users being identified with generic IDs like `null`, `true`, or `distinctId`. + +PostHog also has built-in protections to stop the most common distinct ID mistakes. + +### 3\. Reset after logout + +If a user logs out on your frontend, you should call `reset()` to unlink any future events made on that device with that user. + +This is important if your users are sharing a computer, as otherwise all of those users are grouped together into a single user due to shared cookies between sessions. + +**We strongly recommend you call `reset` on logout even if you don't expect users to share a computer.** + +You can do that like so: + +PostHog AI + +### Web + +```javascript +posthog.reset() +``` + +### iOS + +```swift +PostHogSDK.shared.reset() +``` + +### Android + +```kotlin +PostHog.reset() +``` + +### React Native + +```jsx +posthog.reset() +``` + +### Dart + +```dart +await Posthog().reset(); +``` + +If you *also* want to reset the `device_id` so that the device will be considered a new device in future events, you can pass `true` as an argument: + +Web + +PostHog AI + +```javascript +posthog.reset(true) +``` + +### 4\. Person profiles and properties + +You'll notice that one of the parameters in the `identify` method is a `properties` object. + +This enables you to set [person properties](/docs/product-analytics/person-properties.md). + +Whenever possible, we recommend passing in all person properties you have available each time you call identify, as this ensures their person profile on PostHog is up to date. + +Person properties can also be set being adding a `$set` property to a event `capture` call. + +See our [person properties docs](/docs/product-analytics/person-properties.md) for more details on how to work with them and best practices. + +### 5\. Use deep links between platforms + +We recommend you call `identify` [as soon as you're able](#1-call-identify-as-soon-as-youre-able), typically when a user signs up or logs in. + +This doesn't work if one or both platforms are unauthenticated. Some examples of such cases are: + +- Onboarding and signup flows before authentication. +- Unauthenticated web pages redirecting to authenticated mobile apps. +- Authenticated web apps prompting an app download. + +In these cases, you can use a [deep link](https://developer.android.com/training/app-links/deep-linking) on Android and [universal links](https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app) on iOS to identify users. + +1. Use `posthog.get_distinct_id()` to get the current distinct ID. Even if you cannot call identify because the user is unauthenticated, this will return an anonymous distinct ID generated by PostHog. +2. Add the distinct ID to the deep link as query parameters, along with other properties like UTM parameters. +3. When the user is redirected to the app, parse the deep link and handle the following cases: + +- The mobile app is already authenticated. In this case, call [`posthog.alias()`](/docs/libraries/js/features.md#alias) with the distinct ID from the web. This associates the two distinct IDs as a single person. +- The mobile app is unauthenticated. In this case, call [`posthog.identify()`](/docs/libraries/js/features.md#identifying-users) with the distinct ID from the web so pre-login mobile events stay connected to the web session. When the user later logs in on mobile, call `identify()` again with your canonical user ID. + +As long as you associate the distinct IDs with `posthog.identify()` or `posthog.alias()`, you can track events generated across platforms. + +Here's an example implementation for handling deep links from web to mobile: + +PostHog AI + +### iOS + +```swift +import PostHog +class DeepLinkIdentityManager { + static let shared = DeepLinkIdentityManager() + // MARK: - Deep Link Received + func handleDeepLink(_ url: URL, isAuthenticatedOnMobile: Bool) { + guard let webDistinctId = URLComponents(url: url, resolvingAgainstBaseURL: true)? + .queryItems?.first(where: { $0.name == "ph_distinct_id" })?.value else { + return + } + if isAuthenticatedOnMobile { + // The mobile app already knows the current user. + // Alias the incoming web distinct ID to that user. + PostHogSDK.shared.alias(webDistinctId) + } else { + // Reuse the web distinct ID until login on mobile. + PostHogSDK.shared.identify(webDistinctId) + } + } + // MARK: - Login/Signup + func handleLogin(canonicalUserId: String) { + // Switch from the web distinct ID (or a mobile anon ID) + // to your canonical user ID. + PostHogSDK.shared.identify(canonicalUserId) + // Set user properties, track signup event, etc. + } + func handleLogout() { + PostHogSDK.shared.reset() + } +} +``` + +### Android + +```kotlin +import android.net.Uri +import com.posthog.PostHog +object DeepLinkIdentityManager { + // Deep Link Received + fun handleDeepLink(uri: Uri, isAuthenticatedOnMobile: Boolean) { + val webDistinctId = uri.getQueryParameter("ph_distinct_id") ?: return + if (isAuthenticatedOnMobile) { + // The mobile app already knows the current user. + // Alias the incoming web distinct ID to that user. + PostHog.alias(webDistinctId) + } else { + // Reuse the web distinct ID until login on mobile. + PostHog.identify(webDistinctId) + } + } + // Login/Signup + fun handleLogin(canonicalUserId: String) { + // Switch from the web distinct ID (or a mobile anon ID) + // to your canonical user ID. + PostHog.identify(canonicalUserId) + // Set user properties, track signup event, etc. + } + fun handleLogout() { + PostHog.reset() + } +} +``` + +## Further reading + +- [Identifying users docs](/docs/product-analytics/identify.md) +- [How person processing works](/docs/how-posthog-works/ingestion-pipeline.md#2-person-processing) +- [An introductory guide to identifying users in PostHog](/tutorials/identifying-users-guide.md) + +### Community questions + +Ask a question + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/ruby-on-rails.md b/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/ruby-on-rails.md new file mode 100644 index 00000000..5d03d054 --- /dev/null +++ b/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/ruby-on-rails.md @@ -0,0 +1,604 @@ +# Ruby on Rails - Docs + +PostHog makes it easy to get data about traffic and usage of your Ruby on Rails app. Integrating PostHog enables analytics, custom event capture, feature flags, and automatic exception tracking. + +This guide walks you through integrating PostHog into your Rails app using the [posthog-rails gem](https://github.com/PostHog/posthog-ruby/tree/main/posthog-rails). + +## Beta: integration via LLM + +Install PostHog for Rails in seconds with our wizard by running this prompt with [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt, or by running it in your terminal. + +`npx @posthog/wizard@latest` + +[Learn more](/wizard.md) + +Or, to integrate manually, continue with the rest of this guide. + +## Features + +- **Automatic exception tracking** – Captures unhandled and rescued exceptions +- **ActiveJob instrumentation** – Tracks background job exceptions +- **User context** – Automatically associates exceptions with the current user +- **Smart filtering** – Excludes common Rails exceptions (404s, etc.) by default +- **Request context** – Adds request metadata and optional PostHog tracing header identity/session context to captured events +- **Rails 7.0+ error reporter** – Integrates with Rails' built-in error reporting +- **Log forwarding** – Optionally forwards `Rails.logger` output to [PostHog Logs](/docs/logs.md) over OpenTelemetry, automatically correlated with request context (Ruby 3.3+) + +## Installation + +Add both gems to your Gemfile: + +Gemfile + +PostHog AI + +```ruby +gem 'posthog-ruby', require: 'posthog' +gem 'posthog-rails' +``` + +Then run: + +Terminal + +PostHog AI + +```bash +bundle install +``` + +## Identifying users + +> **Identifying users is required.** Backend events need a `distinct_id` that matches the ID your frontend uses when calling `posthog.identify()`. Without this, backend events are orphaned — they can't be linked to frontend event captures, [session replays](/docs/session-replay.md), [LLM traces](/docs/ai-engineering.md), or [error tracking](/docs/error-tracking.md). +> +> See our guide on [identifying users](/docs/getting-started/identify-users.md) for how to set this up. + +### Generate the initializer + +Run the install generator to create the PostHog initializer: + +Terminal + +PostHog AI + +```bash +rails generate posthog:install +``` + +This creates `config/initializers/posthog.rb` with sensible defaults and documentation. + +## Configuration + +`PostHog.init` creates a single client instance used across your app. Avoid creating multiple `PostHog::Client` instances with the same API key, as this can cause dropped events and inconsistent behavior. + +The generated initializer includes the most common options: + +config/initializers/posthog.rb + +PostHog AI + +```ruby +# Rails-specific configuration +PostHog::Rails.configure do |config| + config.auto_capture_exceptions = true # Enable automatic exception capture (default: false) + config.report_rescued_exceptions = true # Report exceptions Rails rescues (default: false) + config.auto_instrument_active_job = true # Instrument background jobs (default: false) + config.use_tracing_headers = true # Use PostHog tracing headers for identity/session context (default: true) + config.capture_user_context = true # Include authenticated user info in exceptions (default: true) + config.current_user_method = :current_user # Method to get current user (default: :current_user) + config.user_id_method = nil # Method to get ID from user object (default: auto-detect) + # Add additional exceptions to ignore + config.excluded_exceptions = ['MyCustomError'] +end +# Core PostHog client initialization +PostHog.init do |config| + # Required: Your PostHog project API key + config.api_key = '' + # Optional: Your PostHog instance URL + config.host = 'https://us.i.posthog.com' + # Optional: Personal API key for feature flags + config.personal_api_key = 'phx_xxxxxxxxx' + # Maximum number of events to queue before dropping (default: 10000) + config.max_queue_size = 10_000 + # Send events synchronously on the calling thread (default: false) + config.sync_mode = false + # Feature flags polling interval in seconds (default: 30) + config.feature_flags_polling_interval = 30 + # Feature flag request timeout in seconds (default: 3) + config.feature_flag_request_timeout_seconds = 3 + # Error callback to detect misconfiguration + config.on_error = proc { |status, msg| + Rails.logger.error("PostHog error: #{msg}") + } + # Before-send callback to modify or drop events + config.before_send = proc { |event| + event[:properties] ||= {} + event[:properties]['environment'] = Rails.env + event + } + # Disable network calls in test mode + config.test_mode = true if Rails.env.test? +end +``` + +You can find your project token and instance address in [your project settings](https://us.posthog.com/project/settings). + +> **Tip:** Use [`Rails.application.credentials`](https://guides.rubyonrails.org/security.html#custom-credentials) to avoid hardcoding API keys. First, add your keys and then reference them in your initializer: +> +> Terminal +> +> PostHog AI +> +> ```bash +> rails credentials:edit +> ``` +> +> config/credentials.yml.enc +> +> PostHog AI +> +> ```yaml +> posthog: +> api_key: +> host: https://us.i.posthog.com +> personal_api_key: phx_xxxxxxxxx +> ``` +> +> config/initializers/posthog.rb +> +> PostHog AI +> +> ```ruby +> config.api_key = Rails.application.credentials.posthog[:api_key] +> config.host = Rails.application.credentials.posthog[:host] +> config.personal_api_key = Rails.application.credentials.posthog[:personal_api_key] +> ``` + +## Capturing events + +Track custom events anywhere in your Rails app: + +Ruby + +PostHog AI + +```ruby +PostHog.capture({ + distinct_id: current_user.id, + event: 'post_created', + properties: { title: @post.title } +}) +``` + +Identify a user and set their person properties: + +Ruby + +PostHog AI + +```ruby +PostHog.identify({ + distinct_id: current_user.id, + properties: { + email: current_user.email, + plan: current_user.plan + } +}) +``` + +The Rails integration delegates methods like `capture`, `identify`, `alias`, `group_identify`, `evaluate_flags`, `capture_exception`, `flush`, and `shutdown` to the initialized `PostHog::Client`. + +## Request context + +PostHog Rails automatically applies request-scoped context to events captured during web requests. Request metadata such as `$current_url`, `$request_method`, `$request_path`, `$user_agent`, and `$ip` is added to event properties. + +When `use_tracing_headers` is enabled, PostHog tracing headers (`X-PostHog-Distinct-Id` and `X-PostHog-Session-Id`) are also used as default `distinct_id` and `$session_id` values. Explicit `distinct_id` and properties passed to `PostHog.capture` always take precedence. + +If you're using [PostHog JS](/docs/libraries/js.md) on the frontend, configure [`tracing_headers`](/docs/libraries/js/config.md#tracing-headers) for your Rails backend hostname so browser requests include the session and distinct ID headers. + +Tracing headers are client-controlled analytics context, not authentication or authorization. Pass an authenticated `distinct_id` explicitly for security-sensitive server-side decisions. + +Disable tracing header identity/session capture if you do not want client-supplied tracing headers used for server-side events. Request metadata is still captured: + +Ruby + +PostHog AI + +```ruby +PostHog::Rails.config.use_tracing_headers = false +``` + +## Logs + +To set up [PostHog Logs](/docs/logs.md) in your Rails app, follow the [Ruby on Rails logs installation guide](/docs/logs/installation/ruby-on-rails.md). The integration forwards `Rails.logger` output to PostHog Logs over OpenTelemetry, automatically correlated with each request's distinct ID and session ID. Requires Ruby 3.3+. + +## Error tracking + +For full details on setting up error tracking with Rails, see our [Rails error tracking installation guide](/docs/error-tracking/installation/ruby-on-rails.md). + +### Automatic exception tracking + +When `auto_capture_exceptions` is enabled, exceptions are automatically captured: + +Ruby + +PostHog AI + +```ruby +class PostsController < ApplicationController + def show + @post = Post.find(params[:id]) + # Any exception here is automatically captured + end +end +``` + +`report_rescued_exceptions` controls whether exceptions Rails rescues (for example, exceptions rendered by Rails error pages) are captured. Enable it along with `auto_capture_exceptions` for complete error visibility, or leave it disabled to capture only unhandled exceptions. + +### Manual exception capture + +You can also manually capture exceptions: + +Ruby + +PostHog AI + +```ruby +PostHog.capture_exception( + exception, + current_user.id, + { custom_property: 'value' } +) +``` + +If you evaluated feature flags for the request, pass the same snapshot to include matching flag properties on the exception event: + +Ruby + +PostHog AI + +```ruby +flags = PostHog.evaluate_flags(current_user.id) +PostHog.capture_exception( + exception, + current_user.id, + { custom_property: 'value' }, + flags: flags +) +``` + +### Background job exceptions + +When `auto_instrument_active_job` is enabled, ActiveJob exceptions are automatically captured with job context: + +Ruby + +PostHog AI + +```ruby +class EmailJob < ApplicationJob + def perform(user_id) + user = User.find(user_id) + UserMailer.welcome(user).deliver_now + # Exceptions are automatically captured + end +end +``` + +#### Associating jobs with users + +By default, PostHog extracts a `distinct_id` from job arguments by looking for a `user_id` key in hash arguments: + +Ruby + +PostHog AI + +```ruby +# PostHog will automatically use options[:user_id] as the distinct_id +ProcessOrderJob.perform_later(order.id, user_id: current_user.id) +``` + +For more control, use the `posthog_distinct_id` class method. The proc or block receives the same arguments as `perform`: + +Ruby + +PostHog AI + +```ruby +class SendWelcomeEmailJob < ApplicationJob + posthog_distinct_id ->(user, _options) { user.id } + def perform(user, options = {}) + UserMailer.welcome(user).deliver_now + end +end +``` + +You can also use a block: + +Ruby + +PostHog AI + +```ruby +class ProcessOrderJob < ApplicationJob + posthog_distinct_id do |_order, notify_user_id| + notify_user_id + end + def perform(order, notify_user_id) + # Process the order... + end +end +``` + +### Rails 7.0+ error reporter + +PostHog integrates with Rails' built-in error reporting: + +Ruby + +PostHog AI + +```ruby +# These errors are automatically sent to PostHog +Rails.error.handle do + # Code that might raise an error +end +Rails.error.record(exception, context: { user_id: current_user.id }) +``` + +PostHog automatically extracts the user's distinct ID from `user_id` or `distinct_id` in the context hash. Other context keys are included as properties on the exception event. + +### User context + +PostHog Rails automatically captures authenticated user information from your controllers for exceptions. Authenticated Rails user context takes precedence over client-supplied tracing headers for exception identity. + +If your user method has a different name, configure it: + +Ruby + +PostHog AI + +```ruby +PostHog::Rails.config.current_user_method = :logged_in_user +``` + +#### User ID extraction + +By default, PostHog Rails auto-detects the user's distinct ID by trying these methods in order: + +1. `posthog_distinct_id` – Define this on your User model for full control +2. `distinct_id` – Common analytics convention +3. `id` – Standard ActiveRecord primary key +4. `pk` – Primary key alias +5. `uuid` – For UUID-based primary keys + +It also checks hash-like users for `id`, `pk`, and `uuid` keys. + +You can configure a specific method: + +Ruby + +PostHog AI + +```ruby +PostHog::Rails.config.user_id_method = :email +``` + +Or define a method on your User model: + +Ruby + +PostHog AI + +```ruby +class User < ApplicationRecord + def posthog_distinct_id + "user_#{id}" # or external_id, or any unique identifier + end +end +``` + +### Excluded exceptions + +The following exceptions are not reported by default (common 4xx errors): + +- `AbstractController::ActionNotFound` +- `ActionController::BadRequest` +- `ActionController::InvalidAuthenticityToken` +- `ActionController::InvalidCrossOriginRequest` +- `ActionController::MethodNotAllowed` +- `ActionController::NotImplemented` +- `ActionController::ParameterMissing` +- `ActionController::RoutingError` +- `ActionController::UnknownFormat` +- `ActionController::UnknownHttpMethod` +- `ActionDispatch::Http::Parameters::ParseError` +- `ActiveRecord::RecordNotFound` +- `ActiveRecord::RecordNotUnique` + +Add more with: + +Ruby + +PostHog AI + +```ruby +PostHog::Rails.config.excluded_exceptions = ['MyException'] +``` + +## Feature flags + +Evaluate flags once for the current user, then read values from the returned snapshot: + +Ruby + +PostHog AI + +```ruby +class PostsController < ApplicationController + def show + flags = PostHog.evaluate_flags(current_user.id) + if flags.enabled?('new-post-design') + render 'posts/show_new' + else + render 'posts/show' + end + end +end +``` + +For multivariate flags and experiments, use `get_flag`: + +Ruby + +PostHog AI + +```ruby +flags = PostHog.evaluate_flags(current_user.id) +variant = flags.get_flag('checkout-experiment') +if variant == 'test' + # Do something differently +end +``` + +When capturing an event after branching on a flag, pass the same `flags` snapshot so the event includes the exact flag values used by your code: + +Ruby + +PostHog AI + +```ruby +flags = PostHog.evaluate_flags(current_user.id) +PostHog.capture({ + distinct_id: current_user.id, + event: 'checkout_started', + flags: flags.only_accessed +}) +``` + +For local evaluation, ensure you've set `personal_api_key`: + +Ruby + +PostHog AI + +```ruby +config.personal_api_key = Rails.application.credentials.posthog[:personal_api_key] +``` + +See our [Ruby SDK docs](/docs/libraries/ruby.md#local-evaluation) for details on local evaluation with Puma and Unicorn servers. + +> **Note:** `PostHog.is_feature_enabled`, `PostHog.get_feature_flag`, `PostHog.get_feature_flag_result`, `PostHog.get_feature_flag_payload`, and `PostHog.capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `PostHog.evaluate_flags` for new code. + +## Testing + +In your test environment, disable network calls with test mode: + +config/environments/test.rb + +PostHog AI + +```ruby +PostHog.init do |config| + config.api_key = '' + config.test_mode = true +end +``` + +Or in your specs: + +spec/rails\_helper.rb + +PostHog AI + +```ruby +RSpec.configure do |config| + config.before(:each) do + allow(PostHog).to receive(:capture) + end +end +``` + +## Configuration reference + +### Core PostHog options + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| api_key | String | required | Your PostHog project token. | +| host | String | https://us.i.posthog.com | Fully qualified PostHog API host. | +| personal_api_key | String | nil | Personal API key for local feature flag evaluation and remote config payloads. | +| max_queue_size | Integer | 10000 | Maximum number of events to keep in the async queue before dropping new events. | +| test_mode | Boolean | false | Keep events queued and do not send them. Useful for tests. | +| sync_mode | Boolean | false | Send events synchronously on the calling thread. | +| on_error | Proc | no-op | Callback called as on_error.call(status, error). | +| feature_flags_polling_interval | Integer | 30 | Seconds between local feature flag definition polls. | +| feature_flag_request_timeout_seconds | Integer | 3 | Timeout, in seconds, for feature flag requests. | +| before_send | Proc | nil | Callback that receives the event hash before it is queued or sent. Return a modified event hash, or nil to drop the event. | + +The `PostHog.init` block supports the options above. Less common core options like `batch_size`, `disable_singleton_warning`, `skip_ssl_verification`, and `flag_definition_cache_provider` can be passed as an options hash to `PostHog.init(...)`; see the [Ruby SDK docs](/docs/libraries/ruby.md#configuration) for details. + +### Rails-specific options + +Configure these via `PostHog::Rails.configure` or `PostHog::Rails.config`: + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| auto_capture_exceptions | Boolean | false | Automatically capture exceptions. | +| report_rescued_exceptions | Boolean | false | Report exceptions Rails rescues. | +| auto_instrument_active_job | Boolean | false | Capture ActiveJob exceptions with job context. | +| excluded_exceptions | Array | [] | Additional exception class names to ignore. | +| use_tracing_headers | Boolean | true | Use X-PostHog-Distinct-Id and X-PostHog-Session-Id as request-scoped defaults. | +| capture_user_context | Boolean | true | Include authenticated user info in exceptions. | +| current_user_method | Symbol | :current_user | Controller method used to fetch the current user. | +| user_id_method | Symbol | nil | Method used to extract the distinct ID from the user object. Auto-detects when nil. | + +## Troubleshooting + +### Exceptions not being captured + +1. Verify PostHog is initialized: + + Ruby + + PostHog AI + + ```ruby + Rails.console + > PostHog.initialized? + => true + ``` + +2. Check your excluded exceptions list. + +3. Verify middleware is installed: + + Ruby + + PostHog AI + + ```ruby + Rails.application.middleware + ``` + +### User context not working + +1. Verify `current_user_method` matches your controller method. +2. Check that the user object responds to `posthog_distinct_id`, `distinct_id`, `id`, `pk`, or `uuid`. +3. If using a custom identifier, set `PostHog::Rails.config.user_id_method = :your_method`. + +### Feature flags not working + +Ensure you've set `personal_api_key` in your configuration. + +## Next steps + +For any technical questions for how to integrate specific PostHog features into Rails (such as analytics, feature flags, A/B testing, etc.), have a look at our [Ruby SDK docs](/docs/libraries/ruby.md). + +### Community questions + +Ask a question + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/ruby.md b/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/ruby.md new file mode 100644 index 00000000..b4b6ab9b --- /dev/null +++ b/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/ruby.md @@ -0,0 +1,759 @@ +# Ruby - Docs + +The `posthog-ruby` library provides tracking functionality on the server-side for applications built in Ruby. + +It uses an internal queue to make calls fast and non-blocking. It also batches requests and flushes asynchronously, making it perfect to use in any part of your web app or other server-side application that needs performance. + +> **Use a single client instance (singleton)** — Create the PostHog client once and reuse it throughout your application. Multiple client instances with the same API key can cause dropped events and inconsistent behavior. The SDK logs a warning if it detects multiple instances. + +## Installation + +Add this to your `Gemfile`: + +Terminal + +PostHog AI + +```bash +gem "posthog-ruby" +``` + +In your app, set your API key **before** making any calls. If setting a custom `host`, make sure to include the protocol (e.g. `https://`). + +Ruby + +PostHog AI + +```ruby +require 'posthog' +posthog = PostHog::Client.new({ + api_key: "", + host: "https://us.i.posthog.com", + on_error: Proc.new { |status, msg| print msg } +}) +``` + +You can find your project token and instance address in the [project settings](https://app.posthog.com/project/settings) page in PostHog. + +## Configuration + +Initialize the client with your project token before making any calls: + +Ruby + +PostHog AI + +```ruby +require 'posthog' +posthog = PostHog::Client.new({ + api_key: '', + host: 'https://us.i.posthog.com', + on_error: Proc.new { |status, msg| print msg } +}) +``` + +Available client options: + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| api_key | String | required | Your PostHog project token. | +| host | String | https://us.i.posthog.com | Fully qualified PostHog API host. Include the protocol, for example https://us.i.posthog.com or https://eu.i.posthog.com. | +| personal_api_key | String | nil | Personal API key. Required for local feature flag evaluation and remote config payloads. | +| max_queue_size | Integer | 10000 | Maximum number of events to keep in the async queue before dropping new events. | +| batch_size | Integer | 100 | Maximum number of events to send in one async batch. | +| test_mode | Boolean | false | Keep events queued and do not send them. Useful for tests. | +| sync_mode | Boolean | false | Send events synchronously on the calling thread. Useful in forking environments like Sidekiq and Resque. | +| on_error | Proc | no-op | Callback called as on_error.call(status, error) for API or serialization errors. | +| feature_flags_polling_interval | Integer | 30 | Seconds between local feature flag definition polls. | +| feature_flag_request_timeout_seconds | Integer | 3 | Timeout, in seconds, for feature flag requests. | +| before_send | Proc | nil | Callback that receives the event hash before it is queued or sent. Return a modified event hash, or nil to drop the event. | +| disable_singleton_warning | Boolean | false | Suppress warnings about multiple clients with the same API key. Use only when you intentionally need multiple clients. | +| skip_ssl_verification | Boolean | false | Disable SSL certificate verification. Intended only for local development or custom deployments. | +| flag_definition_cache_provider | Object | nil | Provider for distributed feature flag definition caching. See [distributed flag definition caching](#distributed-flag-definition-caching). | + +### Filtering or modifying events before sending + +Use `before_send` to add, modify, or drop events immediately before the SDK queues or sends them: + +Ruby + +PostHog AI + +```ruby +posthog = PostHog::Client.new({ + api_key: '', + before_send: Proc.new do |event| + event[:properties] ||= {} + event[:properties]['environment'] = ENV['RACK_ENV'] + # Return nil to drop the event + event[:properties]['internal_user'] == true ? nil : event + end +}) +``` + +### Flushing and shutting down + +For short-lived scripts, call `flush` before the process exits. Call `shutdown` when your application is stopping to flush pending events and stop background resources. + +Ruby + +PostHog AI + +```ruby +posthog.capture({ distinct_id: 'user_123', event: 'script_finished' }) +posthog.flush +posthog.shutdown +``` + +## Identifying users + +> **Identifying users is required.** Backend events need a `distinct_id` that matches the ID your frontend uses when calling `posthog.identify()`. Without this, backend events are orphaned — they can't be linked to frontend event captures, [session replays](/docs/session-replay.md), [LLM traces](/docs/ai-engineering.md), or [error tracking](/docs/error-tracking.md). +> +> See our guide on [identifying users](/docs/getting-started/identify-users.md) for how to set this up. + +Identify a user and set their person properties with `identify`: + +Ruby + +PostHog AI + +```ruby +posthog.identify({ + distinct_id: 'distinct_id_of_your_user', + properties: { + email: 'john@doe.com', + pro_user: false + } +}) +``` + +## Capturing events + +You can send custom events using `capture`: + +Ruby + +PostHog AI + +```ruby +posthog.capture({ + distinct_id: 'distinct_id_of_the_user', + event: 'user_signed_up' +}) +``` + +> **Tip:** We recommend using a `[object] [verb]` format for your event names, where `[object]` is the entity that the behavior relates to, and `[verb]` is the behavior itself. For example, `project created`, `user signed up`, or `invite sent`. + +### Setting event properties + +Optionally, you can include additional information with the event by including a [properties](/docs/data/events.md#event-properties) object: + +Ruby + +PostHog AI + +```ruby +posthog.capture({ + distinct_id: 'distinct_id_of_the_user', + event: 'user_signed_up', + properties: { + login_type: 'email', + is_free_trial: true + } +}) +``` + +### Sending pageviews + +If you're aiming for a backend-only implementation of PostHog and won't be capturing events from your frontend, you can send `pageviews` from your backend like so: + +Ruby + +PostHog AI + +```ruby +posthog.capture({ + distinct_id: 'distinct_id_of_the_user', + event: '$pageview', + properties: { + '$current_url': 'https://example.com' + } +}) +``` + +`capture` accepts these fields: + +| Field | Type | Description | +| --- | --- | --- | +| distinct_id | String | The user ID. If omitted, framework integrations can provide request context; otherwise the SDK generates a UUID and marks the event as personless. | +| event | String | Event name. Required. | +| properties | Hash | Event properties. | +| groups | Hash | Group analytics mapping from group type to group key. | +| timestamp | Time | When the event occurred. Defaults to the current time. | +| message_id | String | Optional message ID. | +| uuid | String | Optional event UUID used for deduplication. Must be a valid UUID. | +| flags | PostHog::FeatureFlagEvaluations | Snapshot returned by evaluate_flags. Adds $feature/ and $active_feature_flags properties without another /flags request. | +| send_feature_flags | Boolean, Hash, or PostHog::SendFeatureFlagsOptions | Deprecated. Prefer passing flags: from evaluate_flags. | + +## Person profiles and properties + +The Ruby SDK captures identified events by default. These create [person profiles](/docs/data/persons.md). To set [person properties](/docs/data/user-properties.md) in these profiles, include them when capturing an event: + +Ruby + +PostHog AI + +```ruby +posthog.capture({ + distinct_id: 'distinct_id', + event: 'event_name', + properties: { + '$set': { name: 'Max Hedgehog' }, + '$set_once': { initial_url: '/blog' } + } +}) +``` + +For more details on the difference between `$set` and `$set_once`, see our [person properties docs](/docs/data/user-properties.md#what-is-the-difference-between-set-and-set_once). + +To capture [anonymous events](/docs/data/anonymous-vs-identified-events.md) without person profiles, set the event's `$process_person_profile` property to `false`: + +Ruby + +PostHog AI + +```ruby +posthog.capture({ + distinct_id: 'distinct_id', + event: 'event_name', + properties: { + '$process_person_profile': false + } +}) +``` + +## Alias + +Sometimes, you want to assign multiple distinct IDs to a single user. This is helpful when your primary distinct ID is inaccessible. For example, if a distinct ID used on the frontend is not available in your backend. + +In this case, you can use `alias` to assign another distinct ID to the same user. + +Ruby + +PostHog AI + +```ruby +posthog.alias({ + distinct_id: 'distinct_id', + alias: 'alias_id' +}) +``` + +We strongly recommend reading our docs on [alias](/docs/data/identify.md#alias-assigning-multiple-distinct-ids-to-the-same-user) to best understand how to correctly use this method. + +## Feature flags + +PostHog's [feature flags](/docs/feature-flags.md) enable you to safely deploy and roll back new features as well as target specific users and groups with them. + +There are two steps to implement feature flags in Ruby: + +### Step 1: Evaluate flags once + +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload('flag-key') +end +``` + +#### Multivariate feature flags + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +enabled_variant = flags.get_flag('flag-key') +if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload('flag-key') +end +``` + +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_result()`, `posthog.get_feature_flag_payload()`, and `capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user +end +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags, +}) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Ruby + +PostHog AI + +```ruby +# Attach only flags accessed with enabled?() or get_flag() before this call +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only_accessed, +}) +# Attach only specific flags +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), +}) +``` + +`only_accessed` is order-dependent. If you call it before accessing any flags with `enabled?()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Ruby + +PostHog AI + +```ruby +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, +}) +``` + +### Evaluating only specific flags + +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + flag_keys: ['checkout-flow', 'new-dashboard'], +) +``` + +### Evaluating locally only + +If you want to skip the remote `/flags` request and only use locally cached definitions, pass `only_evaluate_locally: true`: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + only_evaluate_locally: true, +) +``` + +### Disabling GeoIP for flag evaluation + +Pass `disable_geoip: true` to disable GeoIP lookup for remote flag evaluation: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + disable_geoip: true, +) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.enabled?()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_the_user', + person_properties: { + property_name: 'value' + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + group_properties: { + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, + }, +) +if flags.enabled?('flag-key') + # Do something differently for this user +end +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +Ruby + +PostHog AI + +```ruby +posthog = PostHog::Client.new({ + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. +}) +``` + +### Legacy single-flag methods + +The following methods are still available during the migration period, but are deprecated. Prefer `evaluate_flags` for new code. + +| Method | Replacement | +| --- | --- | +| posthog.is_feature_enabled(flag_key, distinct_id, ...) | posthog.evaluate_flags(distinct_id, ...).enabled?(flag_key) | +| posthog.get_feature_flag(flag_key, distinct_id, ...) | posthog.evaluate_flags(distinct_id, ...).get_flag(flag_key) | +| posthog.get_feature_flag_payload(flag_key, distinct_id, ...) | posthog.evaluate_flags(distinct_id, ...).get_flag_payload(flag_key) | +| posthog.get_feature_flag_result(flag_key, distinct_id, ...) | posthog.evaluate_flags(distinct_id, ...) and read get_flag / get_flag_payload | +| posthog.capture({ ..., send_feature_flags: true }) | posthog.capture({ ..., flags: flags }) | + +### Local Evaluation + +Evaluating feature flags requires making a request to PostHog for each flag. However, you can improve performance by evaluating flags locally. Instead of making a request for each flag, PostHog will periodically request and store feature flag definitions locally, enabling you to evaluate flags without making additional requests. + +It is best practice to use local evaluation flags when possible, since this enables you to resolve flags faster and with fewer API calls. + +For details on how to implement local evaluation, see our [local evaluation guide](/docs/feature-flags/local-evaluation.md). + +#### Evaluating feature flags locally in unicorn server + +If you have `preload_app true` in your unicorn config, you can use the [`after_fork`](https://www.rubydoc.info/gems/unicorn/Unicorn%2FConfigurator:after_fork) hook (which is part of the unicorn's configuration) to enable the feature flag cache to receive the updates from PostHog. + +Ruby + +PostHog AI + +```ruby +after_fork do |_server, _worker| + $posthog = PostHog::Client.new({ + api_key: '', + personal_api_key: '', + host: 'https://us.i.posthog.com', + on_error: Proc.new { |status, msg| print msg } + }) +end +``` + +#### Evaluating feature flags locally in a Puma server + +If you use Puma with multiple workers, you can use the `on_worker_boot` hook (which is part of Puma's configuration) to enable the feature flag cache to receive updates from PostHog. + +Ruby + +PostHog AI + +```ruby +on_worker_boot do + $posthog = PostHog::Client.new({ + api_key: '', + personal_api_key: '', + host: 'https://us.i.posthog.com', + on_error: Proc.new { |status, msg| print msg } + }) +end +``` + +### Distributed flag definition caching + +`flag_definition_cache_provider` shares locally evaluated feature flag definitions across multiple workers or processes. The provider object must implement: + +- `flag_definitions` – returns cached definitions as a hash with `:flags`, `:group_type_mapping`, and `:cohorts`, or `nil` if empty. +- `should_fetch_flag_definitions?` – returns `true` if this process should fetch fresh definitions from PostHog. +- `on_flag_definitions_received(data)` – stores freshly fetched definitions. +- `shutdown` – releases locks or other resources. + +Ruby + +PostHog AI + +```ruby +posthog = PostHog::Client.new({ + api_key: '', + personal_api_key: '', + flag_definition_cache_provider: my_cache_provider +}) +``` + +### Remote config payloads + +Use `get_remote_config_payload` to fetch the decrypted remote config payload for a flag. This requires `personal_api_key`. + +Ruby + +PostHog AI + +```ruby +payload = posthog.get_remote_config_payload('flag-key') +``` + +## Experiments (A/B tests) + +Since [experiments](/docs/experiments/start-here.md) use feature flags, the code for running an experiment is very similar to the feature flags code: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags('user_distinct_id') +variant = flags.get_flag('experiment-feature-flag-key') +if variant == 'variant-name' + # Do something +end +``` + +It's also possible to [run experiments without using feature flags](/docs/experiments/running-experiments-without-feature-flags.md). + +## Group analytics + +Group analytics allows you to associate an event with a group (e.g. teams, organizations, etc.). Read the [Group Analytics](/docs/user-guides/group-analytics.md) guide for more information. + +> **Note:** This is a paid feature and is not available on the open-source or free cloud plan. Learn more on the [pricing page](/pricing.md). + +Capture an event and associate it with a group: + +Ruby + +PostHog AI + +```ruby +posthog.capture({ + distinct_id: 'distinct_id_of_the_user', + event: 'movie_played', + properties: { + movie_id: '123', + category: 'romcom' + }, + groups: { + 'company': 'company_id_in_your_db' + } +}) +``` + +Update properties on a group: + +Ruby + +PostHog AI + +```ruby +posthog.group_identify({ + group_type: 'company', + group_key: 'company_id_in_your_db', + properties: { + name: 'Awesome Inc.' + } +}) +``` + +The `name` is a special property which is used in the PostHog UI for the name of the group. If you don't specify a `name` property, the group ID will be used instead. + +If the optional `distinct_id` is not provided in the group identify call, it defaults to `$#{group_type}_#{group_key}` (e.g., `$company_company_id_in_your_db` in the example above). This default behavior will result in each group appearing as a separate person in PostHog. To avoid this, it's often more practical to use a consistent `distinct_id`, such as `group_identifier`. + +## Exception capture + +You can capture exceptions using the `posthog-ruby` library. This enables you to see stack traces and debug errors in your application. Learn more in our [error tracking docs](/docs/error-tracking/installation/ruby.md). + +**Using Rails?** + +The [posthog-rails](/docs/libraries/ruby-on-rails.md) gem provides automatic exception capture, ActiveJob instrumentation, and user context out of the box. See our [Rails error tracking guide](/docs/error-tracking/installation/ruby-on-rails.md) for details. + +For non-Rails Ruby applications, you can manually capture exceptions with `capture_exception`: + +Ruby + +PostHog AI + +```ruby +begin + # Code that might raise an exception + raise StandardError, 'Something went wrong' +rescue => e + posthog.capture_exception( + e, + 'user_distinct_id', + { + custom_property: 'custom_value' + } + ) +end +``` + +The `capture_exception` method accepts the following parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| exception | Exception, String, or exception-like object | The exception to capture. Required. | +| distinct_id | String | The distinct ID of the user. Optional; request context can provide a default, otherwise the SDK generates a UUID. | +| additional_properties | Hash | Additional properties to attach to the exception event. Optional. | +| flags | PostHog::FeatureFlagEvaluations | Optional keyword argument. Adds the same feature flag properties as capture({ flags: flags }). | + +You can also override the [fingerprint](/docs/error-tracking/fingerprints.md) to customize how exceptions are grouped into issues: + +Ruby + +PostHog AI + +```ruby +posthog.capture_exception( + e, + 'user_distinct_id', + { + '$exception_fingerprint': 'CustomExceptionGroup' + } +) +``` + +## Debug mode + +The Ruby SDK logs warnings by default. You can change the log level to `DEBUG` to debug the client: + +Ruby + +PostHog AI + +```ruby +posthog.logger.level = Logger::DEBUG +``` + +You can also replace the SDK logger globally: + +Ruby + +PostHog AI + +```ruby +PostHog::Logging.logger = Rails.logger +``` + +## Test helpers + +When `test_mode: true`, events remain queued. You can inspect and clear the queue in tests: + +Ruby + +PostHog AI + +```ruby +posthog = PostHog::Client.new({ api_key: '', test_mode: true }) +posthog.capture({ distinct_id: 'user_123', event: 'test_event' }) +posthog.queued_messages +posthog.dequeue_last_message +posthog.clear +``` + +## Thank you + +This library is largely based on the `analytics-ruby` package. + +### Community questions + +Ask a question + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/apps/basic-integration/rails/fizzy/.gitignore b/apps/basic-integration/rails/fizzy/.gitignore index 38bd80d0..649280ae 100644 --- a/apps/basic-integration/rails/fizzy/.gitignore +++ b/apps/basic-integration/rails/fizzy/.gitignore @@ -41,3 +41,4 @@ /config/credentials/*.key .DS_Store +.env diff --git a/apps/basic-integration/rails/fizzy/Gemfile b/apps/basic-integration/rails/fizzy/Gemfile index 882ecb26..5ce473d5 100644 --- a/apps/basic-integration/rails/fizzy/Gemfile +++ b/apps/basic-integration/rails/fizzy/Gemfile @@ -37,6 +37,10 @@ gem "zip_kit" gem "mittens" gem "useragent", bc: "useragent" +# PostHog analytics +gem "posthog-ruby", require: "posthog" +gem "posthog-rails" + # Operations gem "autotuner" gem "mission_control-jobs" diff --git a/apps/basic-integration/rails/fizzy/app/controllers/account/cancellations_controller.rb b/apps/basic-integration/rails/fizzy/app/controllers/account/cancellations_controller.rb index b6ac82ab..46d06d50 100644 --- a/apps/basic-integration/rails/fizzy/app/controllers/account/cancellations_controller.rb +++ b/apps/basic-integration/rails/fizzy/app/controllers/account/cancellations_controller.rb @@ -2,6 +2,8 @@ class Account::CancellationsController < ApplicationController before_action :ensure_owner def create + PostHog.capture(distinct_id: Current.user.posthog_distinct_id, event: "account_cancelled", properties: { account_id: Current.account.id }) + Current.account.cancel redirect_to session_menu_path(script_name: nil), notice: "Account deleted" end diff --git a/apps/basic-integration/rails/fizzy/app/controllers/application_controller.rb b/apps/basic-integration/rails/fizzy/app/controllers/application_controller.rb index 87c3ab8d..cefa6043 100644 --- a/apps/basic-integration/rails/fizzy/app/controllers/application_controller.rb +++ b/apps/basic-integration/rails/fizzy/app/controllers/application_controller.rb @@ -10,4 +10,10 @@ class ApplicationController < ActionController::Base etag { "v1" } stale_when_importmap_changes allow_browser versions: :modern + + private + def current_user + Current.user + end + helper_method :current_user end diff --git a/apps/basic-integration/rails/fizzy/app/controllers/boards/columns_controller.rb b/apps/basic-integration/rails/fizzy/app/controllers/boards/columns_controller.rb index 4b71e7f3..0fa42819 100644 --- a/apps/basic-integration/rails/fizzy/app/controllers/boards/columns_controller.rb +++ b/apps/basic-integration/rails/fizzy/app/controllers/boards/columns_controller.rb @@ -16,6 +16,8 @@ def show def create @column = @board.columns.create!(column_params) + PostHog.capture(distinct_id: Current.user.posthog_distinct_id, event: "column_created", properties: { column_id: @column.id, board_id: @board.id, column_name: @column.name }) + respond_to do |format| format.turbo_stream format.json { head :created, location: board_column_path(@board, @column, format: :json) } diff --git a/apps/basic-integration/rails/fizzy/app/controllers/boards/publications_controller.rb b/apps/basic-integration/rails/fizzy/app/controllers/boards/publications_controller.rb index 9435bd8b..a05921ab 100644 --- a/apps/basic-integration/rails/fizzy/app/controllers/boards/publications_controller.rb +++ b/apps/basic-integration/rails/fizzy/app/controllers/boards/publications_controller.rb @@ -5,10 +5,14 @@ class Boards::PublicationsController < ApplicationController def create @board.publish + + PostHog.capture(distinct_id: Current.user.posthog_distinct_id, event: "board_published", properties: { board_id: @board.id, board_name: @board.name }) end def destroy @board.unpublish @board.reload + + PostHog.capture(distinct_id: Current.user.posthog_distinct_id, event: "board_unpublished", properties: { board_id: @board.id, board_name: @board.name }) end end diff --git a/apps/basic-integration/rails/fizzy/app/controllers/boards_controller.rb b/apps/basic-integration/rails/fizzy/app/controllers/boards_controller.rb index a8284b30..0404cc7c 100644 --- a/apps/basic-integration/rails/fizzy/app/controllers/boards_controller.rb +++ b/apps/basic-integration/rails/fizzy/app/controllers/boards_controller.rb @@ -28,6 +28,8 @@ def new def create @board = Board.create! board_params.with_defaults(all_access: true) + PostHog.capture(distinct_id: Current.user.posthog_distinct_id, event: "board_created", properties: { board_id: @board.id, board_name: @board.name, all_access: @board.all_access }) + respond_to do |format| format.html { redirect_to board_path(@board) } format.json { head :created, location: board_path(@board, format: :json) } @@ -57,6 +59,8 @@ def update end def destroy + PostHog.capture(distinct_id: Current.user.posthog_distinct_id, event: "board_deleted", properties: { board_id: @board.id, board_name: @board.name }) + @board.destroy respond_to do |format| diff --git a/apps/basic-integration/rails/fizzy/app/controllers/cards/closures_controller.rb b/apps/basic-integration/rails/fizzy/app/controllers/cards/closures_controller.rb index a78b3044..8505a226 100644 --- a/apps/basic-integration/rails/fizzy/app/controllers/cards/closures_controller.rb +++ b/apps/basic-integration/rails/fizzy/app/controllers/cards/closures_controller.rb @@ -6,6 +6,8 @@ def create @card.close refresh_stream_if_needed + PostHog.capture(distinct_id: Current.user.posthog_distinct_id, event: "card_closed", properties: { card_id: @card.id, board_id: @board.id, card_number: @card.number }) + respond_to do |format| format.turbo_stream format.json { head :no_content } @@ -16,6 +18,8 @@ def destroy @card.reopen refresh_stream_after_reopen + PostHog.capture(distinct_id: Current.user.posthog_distinct_id, event: "card_reopened", properties: { card_id: @card.id, board_id: @board.id, card_number: @card.number }) + respond_to do |format| format.turbo_stream format.json { head :no_content } diff --git a/apps/basic-integration/rails/fizzy/app/controllers/cards/comments_controller.rb b/apps/basic-integration/rails/fizzy/app/controllers/cards/comments_controller.rb index f2b31881..6e551866 100644 --- a/apps/basic-integration/rails/fizzy/app/controllers/cards/comments_controller.rb +++ b/apps/basic-integration/rails/fizzy/app/controllers/cards/comments_controller.rb @@ -12,6 +12,8 @@ def index def create @comment = @card.comments.create!(comment_params) + PostHog.capture(distinct_id: Current.user.posthog_distinct_id, event: "comment_created", properties: { card_id: @card.id, board_id: @board.id, comment_id: @comment.id }) + respond_to do |format| format.turbo_stream format.json { head :created, location: card_comment_path(@card, @comment, format: :json) } diff --git a/apps/basic-integration/rails/fizzy/app/controllers/cards/goldnesses_controller.rb b/apps/basic-integration/rails/fizzy/app/controllers/cards/goldnesses_controller.rb index b2b3e619..65d1aa28 100644 --- a/apps/basic-integration/rails/fizzy/app/controllers/cards/goldnesses_controller.rb +++ b/apps/basic-integration/rails/fizzy/app/controllers/cards/goldnesses_controller.rb @@ -4,6 +4,8 @@ class Cards::GoldnessesController < ApplicationController def create @card.gild + PostHog.capture(distinct_id: Current.user.posthog_distinct_id, event: "card_gilded", properties: { card_id: @card.id, board_id: @board.id, card_number: @card.number }) + respond_to do |format| format.turbo_stream { render_card_replacement } format.json { head :no_content } diff --git a/apps/basic-integration/rails/fizzy/app/controllers/cards/not_nows_controller.rb b/apps/basic-integration/rails/fizzy/app/controllers/cards/not_nows_controller.rb index ea91436b..57176bd1 100644 --- a/apps/basic-integration/rails/fizzy/app/controllers/cards/not_nows_controller.rb +++ b/apps/basic-integration/rails/fizzy/app/controllers/cards/not_nows_controller.rb @@ -6,6 +6,8 @@ def create @card.postpone refresh_stream_if_needed + PostHog.capture(distinct_id: Current.user.posthog_distinct_id, event: "card_postponed", properties: { card_id: @card.id, board_id: @board.id, card_number: @card.number }) + respond_to do |format| format.turbo_stream format.json { head :no_content } diff --git a/apps/basic-integration/rails/fizzy/app/controllers/cards/publishes_controller.rb b/apps/basic-integration/rails/fizzy/app/controllers/cards/publishes_controller.rb index a0378eec..f60e843a 100644 --- a/apps/basic-integration/rails/fizzy/app/controllers/cards/publishes_controller.rb +++ b/apps/basic-integration/rails/fizzy/app/controllers/cards/publishes_controller.rb @@ -4,6 +4,8 @@ class Cards::PublishesController < ApplicationController def create @card.publish + PostHog.capture(distinct_id: Current.user.posthog_distinct_id, event: "card_created", properties: { card_id: @card.id, board_id: @board.id, card_number: @card.number }) + if add_another_param? card = @board.cards.create!(status: :drafted) redirect_to card_draft_path(card), notice: "Card added" diff --git a/apps/basic-integration/rails/fizzy/app/controllers/cards_controller.rb b/apps/basic-integration/rails/fizzy/app/controllers/cards_controller.rb index e007527b..40d9a599 100644 --- a/apps/basic-integration/rails/fizzy/app/controllers/cards_controller.rb +++ b/apps/basic-integration/rails/fizzy/app/controllers/cards_controller.rb @@ -19,6 +19,7 @@ def create format.json do card = @board.cards.create! card_params.merge(creator: Current.user, status: "published") + PostHog.capture(distinct_id: Current.user.posthog_distinct_id, event: "card_created", properties: { card_id: card.id, board_id: @board.id }) head :created, location: card_path(card, format: :json) end end diff --git a/apps/basic-integration/rails/fizzy/app/controllers/sessions/magic_links_controller.rb b/apps/basic-integration/rails/fizzy/app/controllers/sessions/magic_links_controller.rb index d2b25594..79511908 100644 --- a/apps/basic-integration/rails/fizzy/app/controllers/sessions/magic_links_controller.rb +++ b/apps/basic-integration/rails/fizzy/app/controllers/sessions/magic_links_controller.rb @@ -44,6 +44,10 @@ def sign_in(magic_link) clear_pending_authentication_token start_new_session_for magic_link.identity + identity = magic_link.identity + PostHog.identify(distinct_id: identity.email_address, properties: { email: identity.email_address }) + PostHog.capture(distinct_id: identity.email_address, event: "user_signed_in", properties: { sign_in_method: "magic_link" }) + respond_to do |format| format.html { redirect_to after_sign_in_url(magic_link) } format.json { render json: { session_token: session_token, requires_signup_completion: requires_signup_completion?(magic_link) } } diff --git a/apps/basic-integration/rails/fizzy/app/controllers/sessions_controller.rb b/apps/basic-integration/rails/fizzy/app/controllers/sessions_controller.rb index cabcfa14..75295e98 100644 --- a/apps/basic-integration/rails/fizzy/app/controllers/sessions_controller.rb +++ b/apps/basic-integration/rails/fizzy/app/controllers/sessions_controller.rb @@ -19,6 +19,10 @@ def create end def destroy + if Current.identity + PostHog.capture(distinct_id: Current.identity.email_address, event: "user_signed_out") + end + terminate_session respond_to do |format| diff --git a/apps/basic-integration/rails/fizzy/app/controllers/signups/completions_controller.rb b/apps/basic-integration/rails/fizzy/app/controllers/signups/completions_controller.rb index 0d45f2f8..71969491 100644 --- a/apps/basic-integration/rails/fizzy/app/controllers/signups/completions_controller.rb +++ b/apps/basic-integration/rails/fizzy/app/controllers/signups/completions_controller.rb @@ -23,6 +23,11 @@ def signup_params end def welcome_to_account + if Current.identity + PostHog.identify(distinct_id: Current.identity.email_address, properties: { email: Current.identity.email_address, name: @signup.full_name }) + PostHog.capture(distinct_id: Current.identity.email_address, event: "signup_completed", properties: { account_id: @signup.account.id }) + end + respond_to do |format| format.html do flash[:welcome_letter] = true diff --git a/apps/basic-integration/rails/fizzy/app/models/user.rb b/apps/basic-integration/rails/fizzy/app/models/user.rb index 842f7f6c..574fc0fd 100644 --- a/apps/basic-integration/rails/fizzy/app/models/user.rb +++ b/apps/basic-integration/rails/fizzy/app/models/user.rb @@ -24,6 +24,14 @@ def deactivate end end + def posthog_distinct_id + identity&.email_address || id + end + + def posthog_properties + { email: identity&.email_address, name: name, role: role, account_id: account_id, created_at: created_at&.iso8601 } + end + def setup? name != identity.email_address end diff --git a/apps/basic-integration/rails/fizzy/app/views/layouts/shared/_head.html.erb b/apps/basic-integration/rails/fizzy/app/views/layouts/shared/_head.html.erb index 4be53645..0ddd91e5 100644 --- a/apps/basic-integration/rails/fizzy/app/views/layouts/shared/_head.html.erb +++ b/apps/basic-integration/rails/fizzy/app/views/layouts/shared/_head.html.erb @@ -25,6 +25,17 @@ <%= yield :head %> + + diff --git a/apps/basic-integration/rails/fizzy/config/initializers/posthog.rb b/apps/basic-integration/rails/fizzy/config/initializers/posthog.rb new file mode 100644 index 00000000..3c4c9097 --- /dev/null +++ b/apps/basic-integration/rails/fizzy/config/initializers/posthog.rb @@ -0,0 +1,15 @@ +PostHog.init do |config| + config.api_key = ENV.fetch("POSTHOG_PROJECT_TOKEN", nil) + config.host = ENV.fetch("POSTHOG_HOST", nil) + config.on_error = proc { |status, msg| Rails.logger.error("PostHog error: #{msg}") } + config.test_mode = true if Rails.env.test? +end + +PostHog::Rails.configure do |config| + config.auto_capture_exceptions = true + config.report_rescued_exceptions = true + config.auto_instrument_active_job = true + config.capture_user_context = true + config.current_user_method = :current_user + config.user_id_method = :posthog_distinct_id +end diff --git a/apps/basic-integration/rails/fizzy/posthog-setup-report.md b/apps/basic-integration/rails/fizzy/posthog-setup-report.md new file mode 100644 index 00000000..d58717b8 --- /dev/null +++ b/apps/basic-integration/rails/fizzy/posthog-setup-report.md @@ -0,0 +1,45 @@ + +# PostHog post-wizard report + +The wizard has completed a deep integration of PostHog analytics into Fizzy. The `posthog-ruby` and `posthog-rails` gems were added and configured, with an initializer at `config/initializers/posthog.rb` that enables automatic exception capture, ActiveJob instrumentation, and user context on errors. The `User` model gained `posthog_distinct_id` (identity email) and `posthog_properties` helper methods. A `current_user` helper was added to `ApplicationController` so that `posthog-rails` can automatically associate exceptions with the authenticated user. The posthog-js frontend snippet was inserted into the shared `` partial, identifying the current user on every page load. Server-side events cover all critical user flows: authentication (sign-in, sign-out), signup completion, board lifecycle (created, deleted, published, unpublished), card lifecycle (created, closed, reopened, postponed, gilded), collaboration (comment created), board structure (column created), and account churn (account cancelled). + +| Event name | Description | File | +|---|---|---| +| `user_signed_in` | A user successfully authenticated via a magic link and started a new session. | `app/controllers/sessions/magic_links_controller.rb` | +| `user_signed_out` | A user explicitly terminated their session by signing out. | `app/controllers/sessions_controller.rb` | +| `signup_completed` | A new user completed sign-up by providing their full name and creating an account. | `app/controllers/signups/completions_controller.rb` | +| `board_created` | A user created a new board to organize work. | `app/controllers/boards_controller.rb` | +| `board_deleted` | A user deleted a board and all its associated data. | `app/controllers/boards_controller.rb` | +| `board_published` | A user published a board to make it publicly accessible via a shareable link. | `app/controllers/boards/publications_controller.rb` | +| `board_unpublished` | A user removed the public publication of a board, making it private again. | `app/controllers/boards/publications_controller.rb` | +| `card_created` | A user created a new card (task/issue) on a board. | `app/controllers/cards/publishes_controller.rb`, `app/controllers/cards_controller.rb` | +| `card_closed` | A user closed a card to mark it as completed. | `app/controllers/cards/closures_controller.rb` | +| `card_reopened` | A user reopened a previously closed card. | `app/controllers/cards/closures_controller.rb` | +| `card_postponed` | A user manually postponed a card to 'not now', removing it from the active backlog. | `app/controllers/cards/not_nows_controller.rb` | +| `comment_created` | A user added a comment to a card. | `app/controllers/cards/comments_controller.rb` | +| `card_gilded` | A user marked a card as golden (high priority) to highlight it for the team. | `app/controllers/cards/goldnesses_controller.rb` | +| `account_cancelled` | An account owner cancelled and deleted the account. | `app/controllers/account/cancellations_controller.rb` | +| `column_created` | A user added a new column to a board to represent a workflow stage. | `app/controllers/boards/columns_controller.rb` | + +## Next steps + +To create the recommended dashboard "Analytics basics (wizard)" in PostHog, navigate to your PostHog project and add the following insights: + +1. **Signup conversion funnel** — Funnel from `user_signed_in` → `signup_completed` to measure what fraction of sign-ins result in a completed account. +2. **Card completion rate** — Funnel from `card_created` → `card_closed` to see how many created cards are eventually resolved. +3. **Weekly card activity** — Trend of `card_created`, `card_closed`, and `comment_created` events over time to measure team engagement. +4. **Postponement vs closure** — Breakdown of `card_closed` vs `card_postponed` to understand how cards leave the active backlog. +5. **Account churn** — Trend of `account_cancelled` over time to monitor retention risk. + +## Verify before merging + +- [ ] Run a full production build (the wizard only verified the files it touched) and fix any lint or type errors introduced by the generated code. +- [ ] Run the test suite — call sites that were rewritten or instrumented may need updated mocks or fixtures. In particular, add stubs for `PostHog.capture` and `PostHog.identify` in controller tests. +- [ ] Add `POSTHOG_PROJECT_TOKEN` and `POSTHOG_HOST` to `.env.example` and any bootstrap scripts so collaborators know what to set. +- [ ] Confirm the returning-visitor path also calls `identify` — the current implementation identifies users on magic-link sign-in and page load (via the frontend snippet), but confirm session resumption for returning users still reaches the `posthog.identify()` call via the layout's `