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 000000000..e69de29bb 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 000000000..8647dcb37 --- /dev/null +++ b/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/identify-users.md @@ -0,0 +1,307 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Identify users - Docs + +Copy page + +# 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/usage.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/usage.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. + +#### Identify users when the web SDK loads + +If your app already knows the signed-in user when you initialize the JavaScript web SDK, the [`loaded` callback](/docs/libraries/js/config.md) is a convenient place to call `identify`. This identifies the user as soon as the SDK has loaded: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + loaded: (posthog) => { + if (currentUser?.id) { + posthog.identify(currentUser.id, { + email: currentUser.email, + name: currentUser.name, + }) + } + }, +}) +``` + +In this example, `currentUser` represents user data already available from your authentication system. If your app loads the user asynchronously, call `posthog.identify()` as soon as that data becomes available instead. + +### 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. + +**\`$set\` and \`$set\_once\` aren't stored on events** + +These properties only tell PostHog how to update person data during ingestion — they aren't kept on the stored event, so you can't filter, break down, or query events by them. To query the values you set, use [person properties](/docs/product-analytics/person-properties.md) instead. + +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/usage.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/usage.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) + +### Still have questions? + +Ask PostHog AI + +### 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 000000000..74838eaf1 --- /dev/null +++ b/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/ruby-on-rails.md @@ -0,0 +1,610 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Ruby on Rails - Docs + +Copy page + +# 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` + +[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). + +### Still have questions? + +Ask PostHog AI + +### 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 000000000..8a2078697 --- /dev/null +++ b/apps/basic-integration/rails/fizzy/.claude/skills/integration-ruby-on-rails/references/ruby.md @@ -0,0 +1,765 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Ruby - Docs + +Copy page + +# 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/product-analytics/person-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/product-analytics/person-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/product-analytics/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. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/apps/basic-integration/rails/fizzy/Gemfile b/apps/basic-integration/rails/fizzy/Gemfile index 882ecb266..14e69cc22 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 +gem "posthog-ruby", "~> 3.21" +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 b6ac82ab4..4fc5989d4 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,7 +2,13 @@ class Account::CancellationsController < ApplicationController before_action :ensure_owner def create - Current.account.cancel + account = Current.account + account.cancel + PostHog.capture( + distinct_id: Current.user.posthog_distinct_id, + event: "account_cancelled", + properties: { account_id: account.id } + ) redirect_to session_menu_path(script_name: nil), notice: "Account deleted" end diff --git a/apps/basic-integration/rails/fizzy/app/controllers/account/exports_controller.rb b/apps/basic-integration/rails/fizzy/app/controllers/account/exports_controller.rb index dbe0a11c1..544710cc6 100644 --- a/apps/basic-integration/rails/fizzy/app/controllers/account/exports_controller.rb +++ b/apps/basic-integration/rails/fizzy/app/controllers/account/exports_controller.rb @@ -10,6 +10,11 @@ def show def create Current.account.exports.create!(user: Current.user).build_later + PostHog.capture( + distinct_id: Current.user.posthog_distinct_id, + event: "account_export_started", + properties: { account_id: Current.account.id } + ) redirect_to account_settings_path, notice: "Export started. You'll receive an email when it's ready." end diff --git a/apps/basic-integration/rails/fizzy/app/controllers/account/imports_controller.rb b/apps/basic-integration/rails/fizzy/app/controllers/account/imports_controller.rb index 1a62f83a0..27e58ffc5 100644 --- a/apps/basic-integration/rails/fizzy/app/controllers/account/imports_controller.rb +++ b/apps/basic-integration/rails/fizzy/app/controllers/account/imports_controller.rb @@ -37,6 +37,11 @@ def start_import(account) Current.set(account: account) do import = account.imports.create!(identity: Current.identity, file: params[:file]) import.process_later + PostHog.capture( + distinct_id: Current.user.posthog_distinct_id, + event: "account_import_started", + properties: { account_id: account.id } + ) end redirect_to account_import_path(import, script_name: account.slug) 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 87c3ab8dd..abfd7e7bb 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 + + helper_method :current_user + + def current_user + Current.user + 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 a8284b309..c981b6fce 100644 --- a/apps/basic-integration/rails/fizzy/app/controllers/boards_controller.rb +++ b/apps/basic-integration/rails/fizzy/app/controllers/boards_controller.rb @@ -27,6 +27,11 @@ 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, all_access: @board.all_access? } + ) respond_to do |format| format.html { redirect_to board_path(@board) } 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 f2b318813..cd29b32a8 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 @@ -11,6 +11,11 @@ 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: @card.board_id } + ) respond_to do |format| format.turbo_stream 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 a0378eec3..ce0195189 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 @@ -3,6 +3,11 @@ class Cards::PublishesController < ApplicationController def create @card.publish + PostHog.capture( + distinct_id: Current.user.posthog_distinct_id, + event: "card_published", + properties: { card_id: @card.id, board_id: @board.id } + ) if add_another_param? card = @board.cards.create!(status: :drafted) 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 e007527b8..f7d9cf988 100644 --- a/apps/basic-integration/rails/fizzy/app/controllers/cards_controller.rb +++ b/apps/basic-integration/rails/fizzy/app/controllers/cards_controller.rb @@ -14,11 +14,21 @@ def create respond_to do |format| format.html do card = Current.user.draft_new_card_in(@board) + PostHog.capture( + distinct_id: Current.user.posthog_distinct_id, + event: "card_created", + properties: { board_id: @board.id, creation_type: "draft" } + ) redirect_to card_draft_path(card) end 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: { board_id: @board.id, creation_type: "published" } + ) head :created, location: card_path(card, format: :json) end end diff --git a/apps/basic-integration/rails/fizzy/app/controllers/join_codes_controller.rb b/apps/basic-integration/rails/fizzy/app/controllers/join_codes_controller.rb index f2ec2588c..bd08b3e79 100644 --- a/apps/basic-integration/rails/fizzy/app/controllers/join_codes_controller.rb +++ b/apps/basic-integration/rails/fizzy/app/controllers/join_codes_controller.rb @@ -14,6 +14,11 @@ def new def create @join_code.redeem_if { |account| @identity.join(account) } user = User.active.find_by!(account: @join_code.account, identity: @identity) + PostHog.capture( + distinct_id: user.posthog_distinct_id, + event: "account_joined", + properties: { account_id: @join_code.account.id } + ) if @identity == Current.identity && user.setup? redirect_to landing_url(script_name: @join_code.account.slug) 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 d2b25594c..9c731198f 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 @@ -43,6 +43,14 @@ def authenticate(magic_link) def sign_in(magic_link) clear_pending_authentication_token start_new_session_for magic_link.identity + unless magic_link.for_sign_up? + identify_identity + PostHog.capture( + distinct_id: Current.identity.id.to_s, + event: "user_signed_in", + properties: { authentication_method: "magic_link" } + ) + end respond_to do |format| format.html { redirect_to after_sign_in_url(magic_link) } @@ -86,4 +94,11 @@ def rate_limit_exceeded def requires_signup_completion?(magic_link) magic_link.for_sign_up? end + + def identify_identity + PostHog.identify( + distinct_id: Current.identity.id.to_s, + properties: { email: Current.identity.email_address } + ) + end end 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 0d45f2f8a..4da5e8aa2 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 @@ -11,6 +11,12 @@ def create @signup = Signup.new(signup_params) if @signup.complete + identify_user + PostHog.capture( + distinct_id: @signup.user.posthog_distinct_id, + event: "user_signed_up", + properties: { account_id: @signup.account.id } + ) welcome_to_account else invalid_signup @@ -22,6 +28,13 @@ def signup_params params.expect(signup: %i[ full_name ]).with_defaults(identity: Current.identity) end + def identify_user + PostHog.identify( + distinct_id: @signup.user.posthog_distinct_id, + properties: @signup.user.posthog_properties + ) + end + def welcome_to_account respond_to do |format| format.html do diff --git a/apps/basic-integration/rails/fizzy/app/models/user.rb b/apps/basic-integration/rails/fizzy/app/models/user.rb index 842f7f6c8..e52839abc 100644 --- a/apps/basic-integration/rails/fizzy/app/models/user.rb +++ b/apps/basic-integration/rails/fizzy/app/models/user.rb @@ -16,6 +16,17 @@ class User < ApplicationRecord has_many :pinned_cards, through: :pins, source: :card has_many :data_exports, class_name: "User::DataExport", dependent: :destroy + def posthog_distinct_id + identity_id.to_s if identity_id.present? + end + + def posthog_properties + { + email: identity&.email_address, + name: name + }.compact + end + def deactivate transaction do accesses.destroy_all 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 000000000..4096b7ab5 --- /dev/null +++ b/apps/basic-integration/rails/fizzy/config/initializers/posthog.rb @@ -0,0 +1,23 @@ +require "posthog" + +posthog_project_token = ENV["POSTHOG_PROJECT_TOKEN"].presence +posthog_host = ENV["POSTHOG_HOST"].presence + +if posthog_project_token && posthog_host + 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 = posthog_project_token + config.host = posthog_host + end +elsif Rails.env.development? + missing_variable = posthog_project_token ? "POSTHOG_HOST" : "POSTHOG_PROJECT_TOKEN" + raise ArgumentError, "#{missing_variable} variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once #{missing_variable} is configured" +end