Skip to content

Conversation

@pauldambra
Copy link
Member

@pauldambra pauldambra commented Dec 12, 2025

we can queue events
and lazy load autocapture
to make library startup cheaper
and bundle smaller

@github-actions
Copy link
Contributor

github-actions bot commented Dec 12, 2025

📝 No Changeset Found

This PR doesn't include a changeset. A changeset (and the release label) is required to release a new version.

How to add a changeset

Run this command and follow the prompts:

pnpm changeset

Remember: Never use major version bumps for posthog-js as it's autoloaded by clients.

Copy link
Member Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

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

@vercel
Copy link

vercel bot commented Dec 12, 2025

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

Project Deployment Review Updated (UTC)
posthog-js Error Error Dec 18, 2025 0:19am
posthog-nextjs-config Ready Ready Preview Dec 18, 2025 0:19am

@github-actions
Copy link
Contributor

github-actions bot commented Dec 12, 2025

@github-actions
Copy link
Contributor

github-actions bot commented Dec 12, 2025

Size Change: -77.1 kB (-1.44%)

Total Size: 5.27 MB

Filename Size Change
packages/browser/dist/all-external-dependencies.js 229 kB -2 B (0%)
packages/browser/dist/array.full.es5.js 298 kB -10.2 kB (-3.31%)
packages/browser/dist/array.full.js 372 kB -4.31 kB (-1.15%)
packages/browser/dist/array.full.no-external.js 388 kB -4.18 kB (-1.07%)
packages/browser/dist/array.js 161 kB -10.1 kB (-5.91%)
packages/browser/dist/array.no-external.js 175 kB -9.9 kB (-5.36%)
packages/browser/dist/dead-clicks-autocapture.js 13 kB -17 B (-0.13%)
packages/browser/dist/main.js 162 kB -10.1 kB (-5.86%)
packages/browser/dist/module.full.js 373 kB -4.31 kB (-1.14%)
packages/browser/dist/module.full.no-external.js 389 kB -4.18 kB (-1.06%)
packages/browser/dist/module.js 162 kB -10.1 kB (-5.87%)
packages/browser/dist/module.no-external.js 176 kB -9.89 kB (-5.32%)
packages/browser/dist/product-tours.js 61.1 kB +168 B (+0.28%)
ℹ️ View Unchanged
Filename Size Change
packages/ai/dist/anthropic/index.cjs 17.8 kB 0 B
packages/ai/dist/anthropic/index.mjs 17.6 kB 0 B
packages/ai/dist/gemini/index.cjs 23.4 kB 0 B
packages/ai/dist/gemini/index.mjs 23.2 kB 0 B
packages/ai/dist/index.cjs 140 kB 0 B
packages/ai/dist/index.mjs 140 kB 0 B
packages/ai/dist/langchain/index.cjs 41.2 kB 0 B
packages/ai/dist/langchain/index.mjs 40.7 kB 0 B
packages/ai/dist/openai/index.cjs 42.3 kB 0 B
packages/ai/dist/openai/index.mjs 42 kB 0 B
packages/ai/dist/vercel/index.cjs 30 kB 0 B
packages/ai/dist/vercel/index.mjs 30 kB 0 B
packages/browser/dist/autocapture.js 13.3 kB 0 B
packages/browser/dist/conversations.js 37.6 kB 0 B
packages/browser/dist/crisp-chat-integration.js 2.11 kB 0 B
packages/browser/dist/customizations.full.js 19.2 kB 0 B
packages/browser/dist/exception-autocapture.js 11.8 kB 0 B
packages/browser/dist/external-scripts-loader.js 2.95 kB 0 B
packages/browser/dist/heatmaps.js 12.2 kB 0 B
packages/browser/dist/intercom-integration.js 2.16 kB 0 B
packages/browser/dist/lazy-recorder.js 152 kB 0 B
packages/browser/dist/posthog-recorder.js 249 kB 0 B
packages/browser/dist/recorder-v2.js 113 kB 0 B
packages/browser/dist/recorder.js 113 kB 0 B
packages/browser/dist/surveys-preview.js 72.9 kB 0 B
packages/browser/dist/surveys.js 85.1 kB 0 B
packages/browser/dist/tracing-headers.js 1.93 kB 0 B
packages/browser/dist/web-vitals.js 10.5 kB 0 B
packages/browser/react/dist/esm/index.js 19.3 kB 0 B
packages/browser/react/dist/umd/index.js 22.4 kB 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.25 kB 0 B
packages/core/dist/error-tracking/coercers/promise-rejection-event.mjs 904 B 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.49 kB 0 B
packages/core/dist/error-tracking/error-properties-builder.mjs 4.15 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.38 kB 0 B
packages/core/dist/error-tracking/parsers/index.mjs 1.94 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.5 kB 0 B
packages/core/dist/featureFlagUtils.mjs 4.28 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 5.7 kB 0 B
packages/core/dist/index.mjs 485 B 0 B
packages/core/dist/posthog-core-stateless.js 29.6 kB 0 B
packages/core/dist/posthog-core-stateless.mjs 27.1 kB 0 B
packages/core/dist/posthog-core.js 28.2 kB 0 B
packages/core/dist/posthog-core.mjs 24 kB 0 B
packages/core/dist/process/index.js 2.77 kB 0 B
packages/core/dist/process/index.mjs 114 B 0 B
packages/core/dist/process/spawn-local.js 1.82 kB 0 B
packages/core/dist/process/spawn-local.mjs 568 B 0 B
packages/core/dist/process/utils.js 3.12 kB 0 B
packages/core/dist/process/utils.mjs 1.15 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 8.2 kB 0 B
packages/core/dist/types.mjs 5.93 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 2 kB 0 B
packages/core/dist/utils/number-utils.mjs 735 B 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 1.91 kB 0 B
packages/core/dist/utils/string-utils.mjs 414 B 0 B
packages/core/dist/utils/type-utils.js 6.93 kB 0 B
packages/core/dist/utils/type-utils.mjs 3.03 kB 0 B
packages/core/dist/utils/user-agent-utils.js 14.9 kB 0 B
packages/core/dist/utils/user-agent-utils.mjs 11.9 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/nextjs-config/dist/config.js 4.97 kB 0 B
packages/nextjs-config/dist/config.mjs 3.48 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 3.83 kB 0 B
packages/nextjs-config/dist/utils.mjs 1.72 kB 0 B
packages/node/dist/client.js 24.4 kB 0 B
packages/node/dist/client.mjs 22.6 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/experimental.js 603 B 0 B
packages/node/dist/experimental.mjs 0 B 0 B 🆕
packages/node/dist/exports.js 3.6 kB 0 B
packages/node/dist/exports.mjs 124 B 0 B
packages/node/dist/extensions/context/context.js 2.12 kB 0 B
packages/node/dist/extensions/context/context.mjs 862 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 3.88 kB 0 B
packages/node/dist/extensions/error-tracking/index.mjs 2.61 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.75 kB 0 B
packages/node/dist/extensions/express.mjs 1.16 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 30.9 kB 0 B
packages/node/dist/extensions/feature-flags/feature-flags.mjs 28.9 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 603 B 0 B
packages/node/dist/types.mjs 0 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.19 kB 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 6.77 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.49 kB 0 B
packages/react-native/dist/hooks/useFeatureFlags.js 821 B 0 B
packages/react-native/dist/hooks/useNavigationTracker.js 2.46 kB 0 B
packages/react-native/dist/hooks/usePostHog.js 467 B 0 B
packages/react-native/dist/index.js 3.12 kB 0 B
packages/react-native/dist/native-deps.js 8.05 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 30.4 kB 0 B
packages/react-native/dist/PostHogContext.js 329 B 0 B
packages/react-native/dist/PostHogProvider.js 4.77 kB 0 B
packages/react-native/dist/storage.js 3.39 kB 0 B
packages/react-native/dist/surveys/components/BottomSection.js 1.34 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.31 kB 0 B
packages/react-native/dist/surveys/components/QuestionTypes.js 10.9 kB 0 B
packages/react-native/dist/surveys/components/SurveyModal.js 3.86 kB 0 B
packages/react-native/dist/surveys/components/Surveys.js 7.18 kB 0 B
packages/react-native/dist/surveys/getActiveMatchingSurveys.js 2.64 kB 0 B
packages/react-native/dist/surveys/icons.js 7.76 kB 0 B
packages/react-native/dist/surveys/index.js 600 B 0 B
packages/react-native/dist/surveys/PostHogSurveyProvider.js 5.66 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.2 kB 0 B
packages/react-native/dist/tooling/posthogMetroSerializer.js 4.78 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 539 B 0 B
packages/react-native/dist/version.js 130 B 0 B
packages/react/dist/esm/index.js 19.3 kB 0 B
packages/react/dist/umd/index.js 22.4 kB 0 B
packages/rollup-plugin/dist/index.js 3.45 kB 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 2.65 kB 0 B
packages/webpack-plugin/dist/config.mjs 1.64 kB 0 B
packages/webpack-plugin/dist/index.js 6.38 kB 0 B
packages/webpack-plugin/dist/index.mjs 2.96 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

@pauldambra pauldambra changed the title feat: lazy load autocapture for fater start and since not everyone has it turned on feat: lazy load autocapture and heatmaps for fater start and since not everyone has it turned on Dec 13, 2025
@pauldambra pauldambra changed the title feat: lazy load autocapture and heatmaps for fater start and since not everyone has it turned on feat: lazy load autocapture and heatmaps for faster start and since not everyone has it turned on Dec 13, 2025
@pauldambra pauldambra requested a review from Copilot December 13, 2025 16:05
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements lazy loading for autocapture and heatmaps features to reduce initial bundle size and improve startup performance. The changes introduce a queuing mechanism that buffers events until the lazy-loaded modules finish loading, ensuring no events are lost during the startup phase.

Key changes:

  • Split autocapture and heatmaps into lightweight wrapper classes and lazy-loaded implementations
  • Added event queuing with a 1000-event cap to handle events before lazy modules load
  • Reorganized utility functions into shared dom-event-utils.ts module
  • Updated test infrastructure to support lazy loading pattern

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
packages/browser/src/extensions/autocapture/index.ts New wrapper class that queues events and lazy loads the full autocapture implementation
packages/browser/src/extensions/heatmaps/index.ts New wrapper class that queues heatmap events and lazy loads the full heatmaps implementation
packages/browser/src/entrypoints/autocapture.ts Lazy-loaded autocapture implementation moved from main bundle
packages/browser/src/entrypoints/heatmaps.ts Lazy-loaded heatmaps implementation moved from main bundle
packages/browser/src/extensions/autocapture/external.ts Autocapture property extraction functions (previously in autocapture.ts)
packages/browser/src/extensions/autocapture/autocapture-utils.ts Autocapture utility functions with updated imports and re-exports
packages/browser/src/utils/dom-event-utils.ts Shared DOM event utilities extracted for reuse across modules
packages/browser/src/heatmaps.ts Backwards-compatible export wrapper for the new heatmaps module
packages/browser/src/autocapture.ts Removed, replaced by modular structure
packages/browser/src/__tests__/extensions/autocapture-wrapper.test.ts New tests for autocapture queuing behavior
packages/browser/src/__tests__/heatmaps.test.ts Updated to mock lazy loading infrastructure
packages/browser/src/__tests__/autocapture.test.ts Updated imports and test expectations for new lazy loading behavior
packages/browser/rollup.config.mjs Added reserved names for cross-bundle function calls
packages/browser/terser-mangled-names.json Updated mangled names list for new architecture
playground/nextjs/pages/inp-test.tsx New test page for measuring Interaction to Next Paint performance
playground/nextjs/package.json Updated to use local browser package for testing

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +151 to +152
if (this._eventQueue.length < MAX_QUEUED_EVENTS) {
this._eventQueue.push({ event: e, timestamp: Date.now() })
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

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

Queuing Event objects could lead to memory leaks if events are not processed quickly. Event objects can hold references to DOM elements and other objects through their target property and event bubbling paths. If many events accumulate in the queue (up to 1000), this could consume significant memory.

Consider extracting only the necessary data from the event (like type, target, timeStamp, clientX/clientY for MouseEvents) and storing that in the queue instead of the full event object. This would reduce memory pressure and prevent potential leaks from long-lived references to DOM elements.

Copilot uses AI. Check for mistakes.
Comment on lines +131 to +133
_onClick(event: MouseEvent, type: string = 'click'): void {
this._queueEvent(event, type)
}
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

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

The method _onClick is prefixed with an underscore suggesting it's private, but it's actually part of the public API (exposed to be called from the wrapper). This inconsistency could cause confusion. Consider either:

  1. Renaming it to onClick (without underscore) since it's part of the public interface
  2. Or adding a comment explaining why it needs to be accessible despite the private naming convention

Copilot uses AI. Check for mistakes.
const maxINP = inpValues.length > 0 ? Math.max(...inpValues).toFixed(2) : 'N/A'

const p75INP =
inpValues.length > 0 ? inpValues.sort((a, b) => a - b)[Math.floor(inpValues.length * 0.75)]?.toFixed(2) : 'N/A'
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

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

The p75 calculation has a bug: calling .sort() on the array mutates the original inpValues array, which affects subsequent calls to this getter. This means that avgINP, maxINP, and p75INP will all work on a sorted array after the first access of p75INP, leading to incorrect results.

Create a copy of the array before sorting using [...inpValues].sort((a, b) => a - b) to avoid mutating the state.

Suggested change
inpValues.length > 0 ? inpValues.sort((a, b) => a - b)[Math.floor(inpValues.length * 0.75)]?.toFixed(2) : 'N/A'
inpValues.length > 0 ? [...inpValues].sort((a, b) => a - b)[Math.floor(inpValues.length * 0.75)]?.toFixed(2) : 'N/A'

Copilot uses AI. Check for mistakes.
Comment on lines +156 to +168
private _loadScript(cb: () => void): void {
if (assignableWindow.__PosthogExtensions__?.initAutocapture) {
cb()
return
}
assignableWindow.__PosthogExtensions__?.loadExternalDependency?.(this.instance, 'autocapture', (err) => {
if (err) {
logger.error('failed to load script', err)
return
}
cb()
})
}
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

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

If loadExternalDependency fails to load the autocapture script (line 162-164), the callback is never called and the queued events remain in the queue indefinitely. This creates a memory leak and means autocapture events will never be processed.

Consider either:

  1. Clearing the event queue when script loading fails
  2. Calling the callback with an error parameter so the caller can handle the failure
  3. Setting a flag to stop queueing new events after a load failure

Copilot uses AI. Check for mistakes.
Comment on lines +185 to +187
for (const { event, type } of queuedEvents) {
try {
this._lazyLoadedHeatmaps._onClick(event, type)
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

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

The queued event timestamp is not being used when processing events. Unlike the autocapture implementation which preserves timestamps for queued events (line 200 in autocapture/index.ts passes new Date(timestamp) as the third parameter), heatmaps discards this information. This could lead to inaccurate timing data for events that were queued before the lazy-loaded module finished loading.

Consider either:

  1. Passing the timestamp to the lazy-loaded heatmaps implementation (though _onClick doesn't currently accept a timestamp parameter)
  2. Or if timestamp preservation is not needed for heatmaps, remove the timestamp field from the QueuedHeatmapEvent interface (line 24) to avoid confusion.

Copilot uses AI. Check for mistakes.
Comment on lines +141 to +142
if (this._eventQueue.length < MAX_QUEUED_EVENTS) {
this._eventQueue.push({ event, type, timestamp: Date.now() })
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

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

Queuing MouseEvent objects could lead to memory leaks if events are not processed quickly. MouseEvent objects can hold references to DOM elements and other objects through their target property and event bubbling paths. If many events accumulate in the queue (up to 1000), this could consume significant memory.

Consider extracting only the necessary data from the event (like clientX, clientY, target, timeStamp) and storing that in the queue instead of the full event object. This matches the pattern commonly used in similar scenarios where event data needs to be preserved but the event object itself should be released.

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +22
beforeEach(() => {
assignableWindow.__PosthogExtensions__ = assignableWindow.__PosthogExtensions__ || {}
assignableWindow.__PosthogExtensions__.loadExternalDependency = jest.fn().mockImplementation((_ph, _name, cb) => {
cb()
})
})
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

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

The mock implementation of loadExternalDependency immediately calls the callback, which means tests won't catch race conditions or timing issues in the event queuing mechanism. Consider adding tests that verify:

  1. Events are properly queued before the lazy module loads
  2. Queued events are correctly processed after lazy load completes
  3. Events go directly to the lazy implementation after it's loaded (bypassing the queue)
  4. Queue size limits are respected

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +193
export class Heatmaps {
instance: PostHog
_enabledServerSide: boolean = false
_initialized = false

private _lazyLoadedHeatmaps: LazyLoadedHeatmapsInterface | undefined
private _eventQueue: QueuedHeatmapEvent[] = []
private _onClickHandler: ((e: Event) => void) | undefined
private _onMouseMoveHandler: ((e: Event) => void) | undefined

constructor(instance: PostHog) {
this.instance = instance
this._enabledServerSide = !!this.instance.persistence?.props[HEATMAPS_ENABLED_SERVER_SIDE]
}

public get flushIntervalMilliseconds(): number {
let flushInterval = DEFAULT_FLUSH_INTERVAL
if (
isObject(this.instance.config.capture_heatmaps) &&
this.instance.config.capture_heatmaps.flush_interval_milliseconds
) {
flushInterval = this.instance.config.capture_heatmaps.flush_interval_milliseconds
}
return flushInterval
}

public get isEnabled(): boolean {
if (!isUndefined(this.instance.config.capture_heatmaps)) {
return this.instance.config.capture_heatmaps !== false
}
if (!isUndefined(this.instance.config.enable_heatmaps)) {
return this.instance.config.enable_heatmaps
}
return this._enabledServerSide
}

public startIfEnabled(): void {
if (this.isEnabled) {
if (this._initialized) {
return
}
this._setupListeners()
this._initialized = true

this._loadScript(() => {
this._start()
})
} else {
this._removeListeners()
if (this._lazyLoadedHeatmaps) {
this._lazyLoadedHeatmaps.stop()
this._lazyLoadedHeatmaps = undefined
}
this._eventQueue = []
}
}

public onRemoteConfig(response: RemoteConfig): void {
const optIn = !!response['heatmaps']

if (this.instance.persistence) {
this.instance.persistence.register({
[HEATMAPS_ENABLED_SERVER_SIDE]: optIn,
})
}
this._enabledServerSide = optIn
this.startIfEnabled()
}

public getAndClearBuffer(): HeatmapEventBuffer {
if (this._lazyLoadedHeatmaps) {
return this._lazyLoadedHeatmaps.getAndClearBuffer()
}
return undefined
}

private _setupListeners(): void {
if (!window || !document) {
return
}

this._onClickHandler = (e) => this._queueEvent((e || window?.event) as MouseEvent, 'click')
addEventListener(document, 'click', this._onClickHandler, { capture: true })

this._onMouseMoveHandler = (e) => this._queueEvent((e || window?.event) as MouseEvent, 'mousemove')
addEventListener(document, 'mousemove', this._onMouseMoveHandler, { capture: true })
}

private _removeListeners(): void {
if (!window || !document) {
return
}

if (this._onClickHandler) {
document.removeEventListener('click', this._onClickHandler, { capture: true })
}

if (this._onMouseMoveHandler) {
document.removeEventListener('mousemove', this._onMouseMoveHandler, { capture: true })
}

this._initialized = false
}

_onClick(event: MouseEvent, type: string = 'click'): void {
this._queueEvent(event, type)
}

private _queueEvent(event: MouseEvent, type: string): void {
if (this._lazyLoadedHeatmaps) {
this._lazyLoadedHeatmaps._onClick(event, type)
return
}

if (this._eventQueue.length < MAX_QUEUED_EVENTS) {
this._eventQueue.push({ event, type, timestamp: Date.now() })
}
}

private _loadScript(cb: () => void): void {
if (assignableWindow.__PosthogExtensions__?.initHeatmaps) {
cb()
return
}
assignableWindow.__PosthogExtensions__?.loadExternalDependency?.(this.instance, 'heatmaps', (err) => {
if (err) {
logger.error('failed to load script', err)
return
}
cb()
})
}

private _start(): void {
if (!document) {
logger.error('`document` not found. Cannot start.')
return
}

if (!this._lazyLoadedHeatmaps && assignableWindow.__PosthogExtensions__?.initHeatmaps) {
this._lazyLoadedHeatmaps = assignableWindow.__PosthogExtensions__.initHeatmaps(this.instance)
this._lazyLoadedHeatmaps.start()

this._processQueuedEvents()
logger.info('started')
}
}

private _processQueuedEvents(): void {
if (!this._lazyLoadedHeatmaps || this._eventQueue.length === 0) {
return
}

logger.info(`processing ${this._eventQueue.length} queued events`)

const queuedEvents = this._eventQueue
this._eventQueue = []

for (const { event, type } of queuedEvents) {
try {
this._lazyLoadedHeatmaps._onClick(event, type)
} catch (error) {
logger.error('Failed to process queued event', error)
}
}
}
}
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

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

The event queuing behavior in the heatmaps wrapper class lacks dedicated test coverage. Similar to autocapture which has autocapture-wrapper.test.ts, consider adding heatmaps-wrapper.test.ts to verify:

  1. Events are properly queued before lazy load completes
  2. Queued events are correctly processed after lazy load
  3. Queue size limits (MAX_QUEUED_EVENTS) are respected
  4. Direct delegation happens after lazy load

Copilot uses AI. Check for mistakes.
Comment on lines +146 to +158
private _loadScript(cb: () => void): void {
if (assignableWindow.__PosthogExtensions__?.initHeatmaps) {
cb()
return
}
assignableWindow.__PosthogExtensions__?.loadExternalDependency?.(this.instance, 'heatmaps', (err) => {
if (err) {
logger.error('failed to load script', err)
return
}
cb()
})
}
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

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

If loadExternalDependency fails to load the heatmaps script (line 152-154), the callback is never called and the queued events remain in the queue indefinitely. This creates a memory leak and means heatmap events will never be processed.

Consider either:

  1. Clearing the event queue when script loading fails
  2. Calling the callback with an error parameter so the caller can handle the failure
  3. Setting a flag to stop queueing new events after a load failure

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Contributor

github-actions bot commented Jan 5, 2026

This PR hasn't seen activity in a week! Should it be merged, closed, or further worked on? If you want to keep it open, post a comment or remove the stale label – otherwise this will be closed in another week.

@github-actions github-actions bot added the stale label Jan 5, 2026
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.

2 participants