Skip to content

feat(replay): account for V2 trigger groups#3281

Open
ksvat wants to merge 1 commit intomainfrom
account-for-new-trigger-groups
Open

feat(replay): account for V2 trigger groups#3281
ksvat wants to merge 1 commit intomainfrom
account-for-new-trigger-groups

Conversation

@ksvat
Copy link
Copy Markdown
Contributor

@ksvat ksvat commented Mar 24, 2026

Problem

Introduce V2 trigger functionality to sdk

Changes

Implements V2 trigger groups for session recording with per-group sampling, per-group minimum duration checking, and per-group conditions.

  1. Union Behavior
  • ALL trigger groups are evaluated independently
  • If ANY group matches conditions AND passes sample rate → recording starts
  • Multiple groups can cause recording simultaneously
  1. Per-Group Sampling
  • Each group makes independent sampling decision based on sessionId + groupId
  • Stored in persistence: $posthog_sr_group_sampling_<groupID>
  • Sampling decisions tracked via custom event: triggerGroupSamplingDecisionMade
  1. Minimum Duration (lowest /quickest wins)
  • Each group can specify minDurationMs
  • Buffer flushes when duration meets the LOWEST minDurationMs among activated groups
  • Example: If Group A (60s) and Group B (1s) both match : flushes after 1s
  1. Logging
  • All matched groups (regardless of sampling) are tracked in:
    $sdk_debug_replay_matched_recording_trigger_groups: [`` ``{ id: '1234', name: 'Error Tracking', matched: true, sampled: true },`` ``{ id: 'premium', name: 'Premium Users', matched: true, sampled: false }`` ``]
  1. Slight Performance Optimization
  • Added _hasCompletedInitialFlush flag:
  • Before flush: Check all triggers on every event (allows later groups with lower minDuration to reduce wait time)
  • After flush: Stop checking triggers

tests

  1. Session properties registration - Verifies tracking of matched/sampled groups
  2. URL blocklist priority - Confirms blocklist still blocks V2 triggers
  3. Union behavior - Multiple groups triggering and being tracked
  4. Empty conditions - Groups with no conditions trigger immediately

Backward Compatibility

V1 behavior is unchanged

  • V2 only activates when version: 2 is present in remote config
  • V1 continues to work exactly as before when version !== 2
  • No breaking changes to existing configurations

Screen Recording 2026-03-24 at 9.33.26 PM.mov (uploaded via Graphite)

Release info Sub-libraries affected

Libraries affected

  • All of them
  • posthog-js (web)
  • posthog-js-lite (web lite)
  • posthog-node
  • posthog-react-native
  • @posthog/react
  • @posthog/ai
  • @posthog/convex
  • @posthog/next
  • @posthog/nextjs-config
  • @posthog/nuxt
  • @posthog/rollup-plugin
  • @posthog/webpack-plugin
  • @posthog/types

Checklist

  • Tests for new code
  • Accounted for the impact of any changes across different platforms
  • Accounted for backwards compatibility of any changes (no breaking changes!)
  • Took care not to unnecessarily increase the bundle size

If releasing new changes

  • Ran pnpm changeset to generate a changeset file
  • Added the "release" label to the PR to indicate we're publishing new versions for the affected packages

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 24, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
posthog-example-next-app-router Error Error Mar 27, 2026 9:17pm
posthog-js Error Error Mar 27, 2026 9:17pm
posthog-nextjs-config Error Error Mar 27, 2026 9:17pm

Request Review

Copy link
Copy Markdown
Contributor Author

ksvat commented Mar 24, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 24, 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 24, 2026

Size Change: +14 kB (+0.21%)

Total Size: 6.59 MB

Filename Size Change
packages/browser/dist/array.full.es5.js 326 kB +103 B (+0.03%)
packages/browser/dist/array.full.js 421 kB +103 B (+0.02%)
packages/browser/dist/array.full.no-external.js 443 kB +103 B (+0.02%)
packages/browser/dist/array.js 177 kB +103 B (+0.06%)
packages/browser/dist/array.no-external.js 193 kB +103 B (+0.05%)
packages/browser/dist/default-extensions.js 175 kB +103 B (+0.06%)
packages/browser/dist/extension-bundles.js 98 kB +103 B (+0.11%)
packages/browser/dist/lazy-recorder.js 157 kB +6.37 kB (+4.23%)
packages/browser/dist/main.js 181 kB +103 B (+0.06%)
packages/browser/dist/module.full.js 424 kB +103 B (+0.02%)
packages/browser/dist/module.full.no-external.js 445 kB +103 B (+0.02%)
packages/browser/dist/module.js 181 kB +103 B (+0.06%)
packages/browser/dist/module.no-external.js 197 kB +103 B (+0.05%)
packages/browser/dist/posthog-recorder.js 257 kB +6.37 kB (+2.54%)
ℹ️ View Unchanged
Filename Size Change
packages/ai/dist/anthropic/index.cjs 20 kB 0 B
packages/ai/dist/anthropic/index.mjs 19.7 kB 0 B
packages/ai/dist/gemini/index.cjs 26.9 kB 0 B
packages/ai/dist/gemini/index.mjs 26.8 kB 0 B
packages/ai/dist/index.cjs 161 kB 0 B
packages/ai/dist/index.mjs 160 kB 0 B
packages/ai/dist/langchain/index.cjs 42.7 kB 0 B
packages/ai/dist/langchain/index.mjs 42.1 kB 0 B
packages/ai/dist/openai/index.cjs 46.7 kB 0 B
packages/ai/dist/openai/index.mjs 46.4 kB 0 B
packages/ai/dist/otel/index.cjs 1.14 kB 0 B
packages/ai/dist/otel/index.mjs 1.08 kB 0 B
packages/ai/dist/vercel/index.cjs 35.4 kB 0 B
packages/ai/dist/vercel/index.mjs 35.3 kB 0 B
packages/browser/dist/all-external-dependencies.js 266 kB 0 B
packages/browser/dist/conversations.js 63.8 kB 0 B
packages/browser/dist/crisp-chat-integration.js 1.88 kB 0 B
packages/browser/dist/customizations.full.js 17.8 kB 0 B
packages/browser/dist/dead-clicks-autocapture.js 13 kB 0 B
packages/browser/dist/element-inference.js 5.59 kB 0 B
packages/browser/dist/exception-autocapture.js 11.7 kB 0 B
packages/browser/dist/external-scripts-loader.js 2.83 kB 0 B
packages/browser/dist/intercom-integration.js 1.93 kB 0 B
packages/browser/dist/logs.js 38.3 kB 0 B
packages/browser/dist/module.slim.js 95 kB 0 B
packages/browser/dist/module.slim.no-external.js 100 kB 0 B
packages/browser/dist/product-tours-preview.js 76.2 kB 0 B
packages/browser/dist/product-tours.js 115 kB 0 B
packages/browser/dist/recorder-v2.js 111 kB 0 B
packages/browser/dist/recorder.js 111 kB 0 B
packages/browser/dist/surveys-preview.js 75.3 kB 0 B
packages/browser/dist/surveys.js 89.8 kB 0 B
packages/browser/dist/tracing-headers.js 1.74 kB 0 B
packages/browser/dist/web-vitals-with-attribution.js 11.8 kB 0 B
packages/browser/dist/web-vitals.js 6.39 kB 0 B
packages/browser/react/dist/esm/index.js 20.7 kB 0 B
packages/browser/react/dist/esm/surveys/index.js 4.54 kB 0 B
packages/browser/react/dist/umd/index.js 24 kB 0 B
packages/browser/react/dist/umd/surveys/index.js 5.49 kB 0 B
packages/convex/dist/client/index.js 7.66 kB 0 B
packages/convex/dist/component/_generated/api.js 712 B 0 B
packages/convex/dist/component/_generated/component.js 212 B 0 B
packages/convex/dist/component/_generated/dataModel.js 230 B 0 B
packages/convex/dist/component/_generated/server.js 3.71 kB 0 B
packages/convex/dist/component/convex.config.js 133 B 0 B
packages/convex/dist/component/lib.js 7.95 kB 0 B
packages/convex/dist/component/schema.js 113 B 0 B
packages/core/dist/error-tracking/chunk-ids.js 2.54 kB 0 B
packages/core/dist/error-tracking/chunk-ids.mjs 1.31 kB 0 B
packages/core/dist/error-tracking/coercers/dom-exception-coercer.js 2.3 kB 0 B
packages/core/dist/error-tracking/coercers/dom-exception-coercer.mjs 993 B 0 B
packages/core/dist/error-tracking/coercers/error-coercer.js 2.02 kB 0 B
packages/core/dist/error-tracking/coercers/error-coercer.mjs 794 B 0 B
packages/core/dist/error-tracking/coercers/error-event-coercer.js 1.76 kB 0 B
packages/core/dist/error-tracking/coercers/error-event-coercer.mjs 513 B 0 B
packages/core/dist/error-tracking/coercers/event-coercer.js 1.82 kB 0 B
packages/core/dist/error-tracking/coercers/event-coercer.mjs 548 B 0 B
packages/core/dist/error-tracking/coercers/index.js 6.79 kB 0 B
packages/core/dist/error-tracking/coercers/index.mjs 326 B 0 B
packages/core/dist/error-tracking/coercers/object-coercer.js 3.46 kB 0 B
packages/core/dist/error-tracking/coercers/object-coercer.mjs 2.07 kB 0 B
packages/core/dist/error-tracking/coercers/primitive-coercer.js 1.67 kB 0 B
packages/core/dist/error-tracking/coercers/primitive-coercer.mjs 419 B 0 B
packages/core/dist/error-tracking/coercers/promise-rejection-event.js 2.59 kB 0 B
packages/core/dist/error-tracking/coercers/promise-rejection-event.mjs 1.25 kB 0 B
packages/core/dist/error-tracking/coercers/string-coercer.js 2.01 kB 0 B
packages/core/dist/error-tracking/coercers/string-coercer.mjs 820 B 0 B
packages/core/dist/error-tracking/coercers/utils.js 2.06 kB 0 B
packages/core/dist/error-tracking/coercers/utils.mjs 716 B 0 B
packages/core/dist/error-tracking/error-properties-builder.js 5.56 kB 0 B
packages/core/dist/error-tracking/error-properties-builder.mjs 4.23 kB 0 B
packages/core/dist/error-tracking/index.js 4.11 kB 0 B
packages/core/dist/error-tracking/index.mjs 152 B 0 B
packages/core/dist/error-tracking/parsers/base.js 1.83 kB 0 B
packages/core/dist/error-tracking/parsers/base.mjs 464 B 0 B
packages/core/dist/error-tracking/parsers/chrome.js 2.73 kB 0 B
packages/core/dist/error-tracking/parsers/chrome.mjs 1.32 kB 0 B
packages/core/dist/error-tracking/parsers/gecko.js 2.47 kB 0 B
packages/core/dist/error-tracking/parsers/gecko.mjs 1.13 kB 0 B
packages/core/dist/error-tracking/parsers/index.js 4.75 kB 0 B
packages/core/dist/error-tracking/parsers/index.mjs 2.1 kB 0 B
packages/core/dist/error-tracking/parsers/node.js 3.94 kB 0 B
packages/core/dist/error-tracking/parsers/node.mjs 2.68 kB 0 B
packages/core/dist/error-tracking/parsers/opera.js 2.26 kB 0 B
packages/core/dist/error-tracking/parsers/opera.mjs 746 B 0 B
packages/core/dist/error-tracking/parsers/safari.js 1.88 kB 0 B
packages/core/dist/error-tracking/parsers/safari.mjs 574 B 0 B
packages/core/dist/error-tracking/parsers/winjs.js 1.72 kB 0 B
packages/core/dist/error-tracking/parsers/winjs.mjs 426 B 0 B
packages/core/dist/error-tracking/types.js 1.33 kB 0 B
packages/core/dist/error-tracking/types.mjs 131 B 0 B
packages/core/dist/error-tracking/utils.js 1.8 kB 0 B
packages/core/dist/error-tracking/utils.mjs 604 B 0 B
packages/core/dist/eventemitter.js 1.78 kB 0 B
packages/core/dist/eventemitter.mjs 571 B 0 B
packages/core/dist/featureFlagUtils.js 6.8 kB 0 B
packages/core/dist/featureFlagUtils.mjs 4.32 kB 0 B
packages/core/dist/gzip.js 1.88 kB 0 B
packages/core/dist/gzip.mjs 577 B 0 B
packages/core/dist/index.js 7.28 kB 0 B
packages/core/dist/index.mjs 707 B 0 B
packages/core/dist/posthog-core-stateless.js 31.3 kB 0 B
packages/core/dist/posthog-core-stateless.mjs 28.8 kB 0 B
packages/core/dist/posthog-core.js 41.1 kB 0 B
packages/core/dist/posthog-core.mjs 36.1 kB 0 B
packages/core/dist/process/cli.js 3.01 kB 0 B
packages/core/dist/process/cli.mjs 1.51 kB 0 B
packages/core/dist/process/config.js 2.65 kB 0 B
packages/core/dist/process/config.mjs 1.41 kB 0 B
packages/core/dist/process/index.js 4.01 kB 0 B
packages/core/dist/process/index.mjs 171 B 0 B
packages/core/dist/process/spawn-local.js 2.17 kB 0 B
packages/core/dist/process/spawn-local.mjs 918 B 0 B
packages/core/dist/process/utils.js 3.27 kB 0 B
packages/core/dist/process/utils.mjs 1.3 kB 0 B
packages/core/dist/surveys/validation.js 3.06 kB 0 B
packages/core/dist/surveys/validation.mjs 1.51 kB 0 B
packages/core/dist/testing/index.js 2.93 kB 0 B
packages/core/dist/testing/index.mjs 79 B 0 B
packages/core/dist/testing/PostHogCoreTestClient.js 3.15 kB 0 B
packages/core/dist/testing/PostHogCoreTestClient.mjs 1.74 kB 0 B
packages/core/dist/testing/test-utils.js 2.77 kB 0 B
packages/core/dist/testing/test-utils.mjs 1.09 kB 0 B
packages/core/dist/types.js 9.5 kB 0 B
packages/core/dist/types.mjs 6.95 kB 0 B
packages/core/dist/utils/bot-detection.js 3.28 kB 0 B
packages/core/dist/utils/bot-detection.mjs 1.95 kB 0 B
packages/core/dist/utils/bucketed-rate-limiter.js 3 kB 0 B
packages/core/dist/utils/bucketed-rate-limiter.mjs 1.62 kB 0 B
packages/core/dist/utils/index.js 11.9 kB 0 B
packages/core/dist/utils/index.mjs 1.98 kB 0 B
packages/core/dist/utils/logger.js 2.5 kB 0 B
packages/core/dist/utils/logger.mjs 1.22 kB 0 B
packages/core/dist/utils/number-utils.js 3.32 kB 0 B
packages/core/dist/utils/number-utils.mjs 1.68 kB 0 B
packages/core/dist/utils/promise-queue.js 2 kB 0 B
packages/core/dist/utils/promise-queue.mjs 768 B 0 B
packages/core/dist/utils/string-utils.js 2.73 kB 0 B
packages/core/dist/utils/string-utils.mjs 1.09 kB 0 B
packages/core/dist/utils/type-utils.js 7.03 kB 0 B
packages/core/dist/utils/type-utils.mjs 3.1 kB 0 B
packages/core/dist/utils/user-agent-utils.js 15.2 kB 0 B
packages/core/dist/utils/user-agent-utils.mjs 12.2 kB 0 B
packages/core/dist/vendor/uuidv7.js 8.29 kB 0 B
packages/core/dist/vendor/uuidv7.mjs 6.72 kB 0 B
packages/next/dist/app/PostHogProvider.js 3.23 kB 0 B
packages/next/dist/client/ClientPostHogProvider.js 1.77 kB 0 B
packages/next/dist/client/hooks.js 174 B 0 B
packages/next/dist/client/PostHogPageView.js 1.7 kB 0 B
packages/next/dist/index.client.js 392 B 0 B
packages/next/dist/index.edge.js 435 B 0 B
packages/next/dist/index.js 426 B 0 B
packages/next/dist/index.react-server.js 411 B 0 B
packages/next/dist/middleware/postHogMiddleware.js 3.62 kB 0 B
packages/next/dist/pages.js 396 B 0 B
packages/next/dist/pages/getServerSidePostHog.js 1.91 kB 0 B
packages/next/dist/pages/PostHogPageView.js 1.2 kB 0 B
packages/next/dist/pages/PostHogProvider.js 1.5 kB 0 B
packages/next/dist/server/getPostHog.js 2.65 kB 0 B
packages/next/dist/server/nodeClientCache.js 1.31 kB 0 B
packages/next/dist/shared/config.js 1.56 kB 0 B
packages/next/dist/shared/constants.js 278 B 0 B
packages/next/dist/shared/cookie.js 4.49 kB 0 B
packages/next/dist/shared/identity.js 264 B 0 B
packages/nextjs-config/dist/config.js 4.97 kB 0 B
packages/nextjs-config/dist/config.mjs 3.49 kB 0 B
packages/nextjs-config/dist/index.js 2.24 kB 0 B
packages/nextjs-config/dist/index.mjs 30 B 0 B
packages/nextjs-config/dist/utils.js 2.93 kB 0 B
packages/nextjs-config/dist/utils.mjs 826 B 0 B
packages/node/dist/client.js 35.9 kB 0 B
packages/node/dist/client.mjs 33.8 kB 0 B
packages/node/dist/entrypoints/index.edge.js 4.25 kB 0 B
packages/node/dist/entrypoints/index.edge.mjs 723 B 0 B
packages/node/dist/entrypoints/index.node.js 5.55 kB 0 B
packages/node/dist/entrypoints/index.node.mjs 1.08 kB 0 B
packages/node/dist/entrypoints/nestjs.js 2.31 kB 0 B
packages/node/dist/entrypoints/nestjs.mjs 42 B 0 B
packages/node/dist/experimental.js 603 B 0 B
packages/node/dist/experimental.mjs 0 B 0 B 🆕
packages/node/dist/exports.js 4.22 kB 0 B
packages/node/dist/exports.mjs 203 B 0 B
packages/node/dist/extensions/context/context.js 2.13 kB 0 B
packages/node/dist/extensions/context/context.mjs 863 B 0 B
packages/node/dist/extensions/context/types.js 603 B 0 B
packages/node/dist/extensions/context/types.mjs 0 B 0 B 🆕
packages/node/dist/extensions/error-tracking/autocapture.js 2.66 kB 0 B
packages/node/dist/extensions/error-tracking/autocapture.mjs 1.24 kB 0 B
packages/node/dist/extensions/error-tracking/index.js 4.14 kB 0 B
packages/node/dist/extensions/error-tracking/index.mjs 2.87 kB 0 B
packages/node/dist/extensions/error-tracking/modifiers/context-lines.node.js 8.81 kB 0 B
packages/node/dist/extensions/error-tracking/modifiers/context-lines.node.mjs 7.15 kB 0 B
packages/node/dist/extensions/error-tracking/modifiers/module.node.js 2.78 kB 0 B
packages/node/dist/extensions/error-tracking/modifiers/module.node.mjs 1.45 kB 0 B
packages/node/dist/extensions/express.js 2.84 kB 0 B
packages/node/dist/extensions/express.mjs 1.25 kB 0 B
packages/node/dist/extensions/feature-flags/cache.js 603 B 0 B
packages/node/dist/extensions/feature-flags/cache.mjs 0 B 0 B 🆕
packages/node/dist/extensions/feature-flags/crypto.js 1.57 kB 0 B
packages/node/dist/extensions/feature-flags/crypto.mjs 395 B 0 B
packages/node/dist/extensions/feature-flags/feature-flags.js 38.8 kB 0 B
packages/node/dist/extensions/feature-flags/feature-flags.mjs 36.7 kB 0 B
packages/node/dist/extensions/nestjs.js 4.39 kB 0 B
packages/node/dist/extensions/nestjs.mjs 2.65 kB 0 B
packages/node/dist/extensions/sentry-integration.js 4.66 kB 0 B
packages/node/dist/extensions/sentry-integration.mjs 3.17 kB 0 B
packages/node/dist/storage-memory.js 1.52 kB 0 B
packages/node/dist/storage-memory.mjs 297 B 0 B
packages/node/dist/types.js 1.43 kB 0 B
packages/node/dist/types.mjs 224 B 0 B
packages/node/dist/version.js 1.21 kB 0 B
packages/node/dist/version.mjs 46 B 0 B
packages/nuxt/dist/module.mjs 4.59 kB 0 B
packages/nuxt/dist/runtime/composables/useFeatureFlagEnabled.js 566 B 0 B
packages/nuxt/dist/runtime/composables/useFeatureFlagPayload.js 690 B 0 B
packages/nuxt/dist/runtime/composables/useFeatureFlagVariantKey.js 591 B 0 B
packages/nuxt/dist/runtime/composables/usePostHog.js 128 B 0 B
packages/nuxt/dist/runtime/nitro-plugin.js 1.08 kB 0 B
packages/nuxt/dist/runtime/vue-plugin.js 1.14 kB 0 B
packages/react-native/dist/autocapture.js 5.05 kB 0 B
packages/react-native/dist/error-tracking/index.js 7.24 kB 0 B
packages/react-native/dist/error-tracking/utils.js 2.58 kB 0 B
packages/react-native/dist/frameworks/wix-navigation.js 1.3 kB 0 B
packages/react-native/dist/hooks/useFeatureFlag.js 1.7 kB 0 B
packages/react-native/dist/hooks/useFeatureFlagResult.js 963 B 0 B
packages/react-native/dist/hooks/useFeatureFlags.js 921 B 0 B
packages/react-native/dist/hooks/useNavigationTracker.js 2.45 kB 0 B
packages/react-native/dist/hooks/usePostHog.js 544 B 0 B
packages/react-native/dist/hooks/utils.js 988 B 0 B
packages/react-native/dist/index.js 4.33 kB 0 B
packages/react-native/dist/native-deps.js 8.77 kB 0 B
packages/react-native/dist/optional/OptionalAsyncStorage.js 299 B 0 B
packages/react-native/dist/optional/OptionalExpoApplication.js 377 B 0 B
packages/react-native/dist/optional/OptionalExpoDevice.js 347 B 0 B
packages/react-native/dist/optional/OptionalExpoFileSystem.js 386 B 0 B
packages/react-native/dist/optional/OptionalExpoFileSystemLegacy.js 423 B 0 B
packages/react-native/dist/optional/OptionalExpoLocalization.js 383 B 0 B
packages/react-native/dist/optional/OptionalReactNativeDeviceInfo.js 415 B 0 B
packages/react-native/dist/optional/OptionalReactNativeLocalize.js 303 B 0 B
packages/react-native/dist/optional/OptionalReactNativeNavigation.js 415 B 0 B
packages/react-native/dist/optional/OptionalReactNativeNavigationWix.js 443 B 0 B
packages/react-native/dist/optional/OptionalReactNativeSafeArea.js 644 B 0 B
packages/react-native/dist/optional/OptionalSessionReplay.js 455 B 0 B
packages/react-native/dist/posthog-rn.js 39.7 kB 0 B
packages/react-native/dist/PostHogContext.js 329 B 0 B
packages/react-native/dist/PostHogErrorBoundary.js 3.19 kB 0 B
packages/react-native/dist/PostHogMaskView.js 1.66 kB 0 B
packages/react-native/dist/PostHogProvider.js 4.76 kB 0 B
packages/react-native/dist/storage.js 4.49 kB 0 B
packages/react-native/dist/surveys/components/BottomSection.js 1.46 kB 0 B
packages/react-native/dist/surveys/components/Cancel.js 909 B 0 B
packages/react-native/dist/surveys/components/ConfirmationMessage.js 1.65 kB 0 B
packages/react-native/dist/surveys/components/QuestionHeader.js 1.37 kB 0 B
packages/react-native/dist/surveys/components/QuestionTypes.js 12.7 kB 0 B
packages/react-native/dist/surveys/components/SurveyModal.js 4.01 kB 0 B
packages/react-native/dist/surveys/components/Surveys.js 7.22 kB 0 B
packages/react-native/dist/surveys/getActiveMatchingSurveys.js 2.64 kB 0 B
packages/react-native/dist/surveys/icons.js 8.86 kB 0 B
packages/react-native/dist/surveys/index.js 600 B 0 B
packages/react-native/dist/surveys/PostHogSurveyProvider.js 5.71 kB 0 B
packages/react-native/dist/surveys/surveys-utils.js 12.7 kB 0 B
packages/react-native/dist/surveys/useActivatedSurveys.js 3.67 kB 0 B
packages/react-native/dist/surveys/useSurveyStorage.js 2.16 kB 0 B
packages/react-native/dist/tooling/expoconfig.js 2.63 kB 0 B
packages/react-native/dist/tooling/metroconfig.js 2.32 kB 0 B
packages/react-native/dist/tooling/posthogMetroSerializer.js 4.86 kB 0 B
packages/react-native/dist/tooling/utils.js 4.05 kB 0 B
packages/react-native/dist/tooling/vendor/expo/expoconfig.js 70 B 0 B
packages/react-native/dist/tooling/vendor/metro/countLines.js 237 B 0 B
packages/react-native/dist/tooling/vendor/metro/utils.js 3.35 kB 0 B
packages/react-native/dist/types.js 70 B 0 B
packages/react-native/dist/utils.js 1.14 kB 0 B
packages/react-native/dist/version.js 130 B 0 B
packages/react/dist/esm/index.js 20.7 kB 0 B
packages/react/dist/esm/surveys/index.js 4.54 kB 0 B
packages/react/dist/umd/index.js 24 kB 0 B
packages/react/dist/umd/surveys/index.js 5.49 kB 0 B
packages/rollup-plugin/dist/index.js 2.11 kB 0 B
packages/types/dist/capture.js 603 B 0 B
packages/types/dist/capture.mjs 0 B 0 B 🆕
packages/types/dist/common.js 603 B 0 B
packages/types/dist/common.mjs 0 B 0 B 🆕
packages/types/dist/feature-flags.js 603 B 0 B
packages/types/dist/feature-flags.mjs 0 B 0 B 🆕
packages/types/dist/index.js 603 B 0 B
packages/types/dist/index.mjs 0 B 0 B 🆕
packages/types/dist/posthog-config.js 603 B 0 B
packages/types/dist/posthog-config.mjs 0 B 0 B 🆕
packages/types/dist/posthog.js 603 B 0 B
packages/types/dist/posthog.mjs 0 B 0 B 🆕
packages/types/dist/request.js 603 B 0 B
packages/types/dist/request.mjs 0 B 0 B 🆕
packages/types/dist/segment.js 603 B 0 B
packages/types/dist/segment.mjs 0 B 0 B 🆕
packages/types/dist/session-recording.js 603 B 0 B
packages/types/dist/session-recording.mjs 0 B 0 B 🆕
packages/types/dist/survey.js 603 B 0 B
packages/types/dist/survey.mjs 0 B 0 B 🆕
packages/types/dist/toolbar.js 603 B 0 B
packages/types/dist/toolbar.mjs 0 B 0 B 🆕
packages/types/dist/tree-shakeable.js 603 B 0 B
packages/types/dist/tree-shakeable.mjs 0 B 0 B 🆕
packages/web/dist/index.cjs 13.8 kB 0 B
packages/web/dist/index.mjs 13.7 kB 0 B
packages/webpack-plugin/dist/config.js 1.52 kB 0 B
packages/webpack-plugin/dist/config.mjs 543 B 0 B
packages/webpack-plugin/dist/index.js 5.38 kB 0 B
packages/webpack-plugin/dist/index.mjs 2.04 kB 0 B
tooling/changelog/dist/index.js 3.31 kB 0 B
tooling/rollup-utils/dist/index.js 1.17 kB 0 B

compressed-size-action

@ksvat ksvat force-pushed the account-for-new-trigger-groups branch from 5d6ec50 to c5ce1cd Compare March 25, 2026 21:12
@ksvat ksvat force-pushed the account-for-new-trigger-groups branch from c5ce1cd to 755ee64 Compare March 25, 2026 23:33
@ksvat ksvat force-pushed the account-for-new-trigger-groups branch from 755ee64 to 5182796 Compare March 26, 2026 15:53
@ksvat ksvat force-pushed the account-for-new-trigger-groups branch from 5182796 to 673a1da Compare March 26, 2026 16:34
@ksvat ksvat added the release label Mar 26, 2026 — with Graphite App
@ksvat ksvat requested review from a team, TueHaulund and fasyy612 and removed request for a team March 26, 2026 16:34
@ksvat ksvat removed request for TueHaulund and fasyy612 March 26, 2026 16:44
@ksvat ksvat requested review from a team, TueHaulund, fasyy612 and pauldambra and removed request for a team March 26, 2026 17:45
@ksvat ksvat marked this pull request as ready for review March 26, 2026 17:45
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 26, 2026

Important Files Changed

Filename Overview
packages/browser/src/extensions/replay/external/triggerMatching.ts Adds TriggerGroupMatching, AlwaysActivatedTriggerMatching, RecordingTriggersStatusV2, and triggerGroupsMatchSessionRecordingStatus for V2 support; existing EventTriggerMatching and URLTriggerMatching use shared persistence keys that cause cross-group contamination when reused per-group.
packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts Wires up V2 trigger group setup, sampling decisions, and status routing; contains the P0 shared-key contamination bug and P1 minimum-duration scope issue.
packages/browser/src/extensions/replay/session-recording.ts Passes V2 version and triggerGroups fields from remote config to persisted config — straightforward and correct.
packages/browser/src/types.ts Adds SessionRecordingTriggerGroup and SessionRecordingTriggerGroupsConfig types; the latter is unused.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts
Line: 1686-1692

Comment:
**Cross-group trigger state contamination via shared persistence keys**

When any V2 group's event trigger fires, `_activateTrigger('event', ...)` is called, which writes `SESSION_RECORDING_EVENT_TRIGGER_ACTIVATED_SESSION = sessionId` to shared persistence. Every other group's `EventTriggerMatching._eventTriggerStatus()` reads that **same** shared key to determine its own activation state. The result is that activating group A via `$exception` will also cause every other group with event triggers to report `TRIGGER_ACTIVATED`, regardless of whether their own event conditions were met.

Concrete example: Group A has `events: ['$exception']`, Group B has `events: ['$pageview']`.
1. `$exception` fires → Group A matches → `_activateTrigger('event')``SESSION_RECORDING_EVENT_TRIGGER_ACTIVATED_SESSION = sessionId`
2. On the next status check, Group B's `EventTriggerMatching._eventTriggerStatus()` checks that same key, sees `sessionId`, and returns `TRIGGER_ACTIVATED`
3. Group B is incorrectly activated — and if Group B's sampling decision was `true`, recording starts as `SAMPLED` even though `$pageview` was never captured

The same cross-contamination applies to URL triggers via `SESSION_RECORDING_URL_TRIGGER_ACTIVATED_SESSION`.

To fix this, each group's trigger activation state needs to be tracked with a **group-scoped** persistence key (e.g. `SESSION_RECORDING_EVENT_TRIGGER_ACTIVATED_SESSION_${groupId}`) instead of the single global key, or the `TriggerGroupMatching.triggerStatus()` path needs to bypass the shared persistence lookup entirely and track activation in per-group local state.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts
Line: 413-424

Comment:
**`_minimumDuration` includes trigger-pending groups, not just activated ones**

The PR description states: *"Buffer flushes when duration meets the LOWEST `minDurationMs` among **activated** groups."* However the current check keeps any group that is not `trigger_disabled` (i.e., both `trigger_activated` **and** `trigger_pending`):

```typescript
if (groupStatus !== 'trigger_disabled') {  // also includes TRIGGER_PENDING
```

This means a group whose conditions have not been met yet can lower (or raise) the effective minimum duration, preventing the buffer from flushing at the right time.

Consider restricting this to activated groups only:

```suggestion
                if (groupStatus === 'trigger_activated') {
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/browser/src/types.ts
Line: 517-523

Comment:
**Unused exported interface**

`SessionRecordingTriggerGroupsConfig` is exported but never referenced anywhere in the codebase. If it is not needed as part of the public API or future planning, it should be removed to keep the surface area clean (simplicity rule: no superfluous parts).

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "feat(replay): account for V2 trigger gro..." | Re-trigger Greptile

Comment on lines +413 to +424
for (const matcher of this._triggerGroupMatchers) {
const groupStatus = matcher.triggerStatus(this.sessionId)

// Only consider groups that are activated or pending
if (groupStatus !== 'trigger_disabled') {
const groupDuration = matcher.group.minDurationMs
if (isNumber(groupDuration)) {
if (isNull(lowestDuration) || groupDuration < lowestDuration) {
lowestDuration = groupDuration
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 _minimumDuration includes trigger-pending groups, not just activated ones

The PR description states: "Buffer flushes when duration meets the LOWEST minDurationMs among activated groups." However the current check keeps any group that is not trigger_disabled (i.e., both trigger_activated and trigger_pending):

if (groupStatus !== 'trigger_disabled') {  // also includes TRIGGER_PENDING

This means a group whose conditions have not been met yet can lower (or raise) the effective minimum duration, preventing the buffer from flushing at the right time.

Consider restricting this to activated groups only:

Suggested change
for (const matcher of this._triggerGroupMatchers) {
const groupStatus = matcher.triggerStatus(this.sessionId)
// Only consider groups that are activated or pending
if (groupStatus !== 'trigger_disabled') {
const groupDuration = matcher.group.minDurationMs
if (isNumber(groupDuration)) {
if (isNull(lowestDuration) || groupDuration < lowestDuration) {
lowestDuration = groupDuration
}
}
}
if (groupStatus === 'trigger_activated') {
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts
Line: 413-424

Comment:
**`_minimumDuration` includes trigger-pending groups, not just activated ones**

The PR description states: *"Buffer flushes when duration meets the LOWEST `minDurationMs` among **activated** groups."* However the current check keeps any group that is not `trigger_disabled` (i.e., both `trigger_activated` **and** `trigger_pending`):

```typescript
if (groupStatus !== 'trigger_disabled') {  // also includes TRIGGER_PENDING
```

This means a group whose conditions have not been met yet can lower (or raise) the effective minimum duration, preventing the buffer from flushing at the right time.

Consider restricting this to activated groups only:

```suggestion
                if (groupStatus === 'trigger_activated') {
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +517 to +523
/**
* V2 Trigger Groups configuration from remote config
*/
export interface SessionRecordingTriggerGroupsConfig {
version: 2
groups: SessionRecordingTriggerGroup[]
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unused exported interface

SessionRecordingTriggerGroupsConfig is exported but never referenced anywhere in the codebase. If it is not needed as part of the public API or future planning, it should be removed to keep the surface area clean (simplicity rule: no superfluous parts).

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/browser/src/types.ts
Line: 517-523

Comment:
**Unused exported interface**

`SessionRecordingTriggerGroupsConfig` is exported but never referenced anywhere in the codebase. If it is not needed as part of the public API or future planning, it should be removed to keep the surface area clean (simplicity rule: no superfluous parts).

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Member

@pauldambra pauldambra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Bot review based on Paul D'Ambra's review patterns and c2 wiki XP principles. Two perspectives below — Paul's usual concerns (coupling, observability, safe rollouts) and an XP lens (simplicity rules, code smells). Comments are inline.

eventTriggers: group.conditions.events,
linkedFlag: group.conditions.flag,
urlBlocklist: [], // groups don't have blocklist
} as any
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from paul's review patterns:

the as any cast here means if someone renames urlTriggers to urlPatterns or whatever, the compiler won't tell you this broke.

i wonder if it's worth making a narrow type that represents just the fields the individual matchers actually need? or even just as Pick<SessionRecordingRemoteConfig, 'urlTriggers' | 'eventTriggers' | ...>? that way the compiler is still in the loop.

not blocking — the tests would catch it — but i'm slightly allergic to as any in code that's this important to get right.


🤖 from c2 wiki / XP lens:

the deeper issue is that URLTriggerMatching, EventTriggerMatching, and LinkedFlagMatching each accept the full remote config bag and reach in for the 2 fields they care about — that's Feature Envy. if each matcher accepted just its own slice (EventTriggerMatching.onConfig({ eventTriggers: string[] })), TriggerGroupMatching could pass the right data without a cast. bigger change, not necessarily for this PR, but it's the direction that removes the as any properly.

anyGroupActivated = true
// Check if this group's sample rate hit
const groupId = matcher.group.id
if (samplingResults.get(groupId) === true) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from c2 wiki / XP lens:

what's the intended meaning when a group is TRIGGER_ACTIVATED but samplingResults has no entry? the code returns ACTIVE, and the edge case test covers it, but the intent is ambiguous: does ACTIVE mean "we're recording but not because of sampling" or "something went wrong and we're recording as a fallback"?

if it truly shouldn't happen, a log warning would surface the bug. if it can happen (race between trigger activation and sampling decision), the behavior should be expressed in the function name or structure rather than relying on the comment.

/**
* V2 Trigger Groups configuration from remote config
*/
export interface SessionRecordingTriggerGroupsConfig {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from c2 wiki / XP lens:

SessionRecordingTriggerGroupsConfig is defined here but not imported or used anywhere. Rule 4 of Simple Design: no superfluous parts. If you need it later, you'll write it then and it'll match what you actually need. YAGNI says delete it.

})

describe('triggerGroupsMatchSessionRecordingStatus - Basic States', () => {
const createMockMatcher = (
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from both paul's review patterns and c2 wiki:

createMockMatcher is defined three times across the describe blocks in this file with minor variations (one adds a sampleRate param). OnceAndOnlyOnce — a single version with optional overrides would deduplicate:

const createMockMatcher = (
    id: string,
    triggerStatus: TriggerStatus,
    overrides?: Partial<{ sampleRate: number }>
): TriggerGroupMatching => ({
    group: { id, name: `Group ${id}`, sampleRate: overrides?.sampleRate ?? 1.0, conditions: { matchType: 'any' } },
    triggerStatus: () => triggerStatus,
    stop: () => {},
} as unknown as TriggerGroupMatching)

also — these tests are excellent candidates for parameterized tests (test.each). each test is: given these inputs, expect this status. that's a table, not a list of test functions 🚢

if (isV2) {
// V2: Setup trigger group matchers
this._statusMatcher = triggerGroupsMatchSessionRecordingStatus
this._setupTriggerGroups(config.triggerGroups!)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from c2 wiki / XP lens:

the V1/V2 branching in _onRemoteConfig, status, _minimumDuration, _flushBuffer, and URL trigger checking is a Shotgun Surgery smell — one concept (V2 trigger groups) is smeared across many methods.

ComposedMethod / Strategy pattern suggests: if every method asks "am I V1 or V2?", you likely have two strategies wanting to be separate objects. extracting V2 behavior into something like a TriggerGroupRecordingStrategy that owns the matchers, sampling results, flush flag, and min-duration logic would mean the V1/V2 branch exists in exactly one place. LazyLoadedSessionRecording is already a Large Class — every future V2 feature will add another if/else to every method.

not a "you must" — it works. but worth considering before V3 arrives 😄

Copy link
Copy Markdown
Contributor

@TueHaulund TueHaulund left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had a couple of questions

simpleEventEmitter.emit('eventCaptured', { event: 'test_event' })

// Should be ACTIVE (matched but sampled out), not SAMPLED
expect(sessionRecording.status).toBe('active')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, why is the session recording status ACTIVE if the session was not sampled?

Comment on lines 200 to 202
// _statusMatcher removed in favor of direct function calls in status getter
expect(sessionRecording['_lazyLoadedSessionRecording']['_triggerMatching']).toBeInstanceOf(
OrTriggerMatching
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is checking for the removed _statusMatcher property. It should be updated to check the _strategy property type instead. Replace with: expect(sessionRecording['_lazyLoadedSessionRecording']['_strategy']).toBeInstanceOf(V1RecordingStrategy)

Spotted by Graphite (based on CI logs)

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants