Skip to content

fix(sdk): prevent infinite recursion with proxy#3112

Merged
adboio merged 1 commit intomainfrom
02-17-fix_sdk_prevent_infinite_recursion_with_proxy
Feb 18, 2026
Merged

fix(sdk): prevent infinite recursion with proxy#3112
adboio merged 1 commit intomainfrom
02-17-fix_sdk_prevent_infinite_recursion_with_proxy

Conversation

@adboio
Copy link
Contributor

@adboio adboio commented Feb 17, 2026

Problem

tl;dr

  • tiktok's in-app browser wraps window.posthog in a proxy, so:
  • Proxy intercepts posthog calls like capture, converts them to posthog.push()
  • .push() calls _execute_array(), which dispatches methods via this[name].apply()
  • this is the Proxy, so the Proxy intercepts again, creating an infinite loop

see https://posthoghelp.zendesk.com/agent/tickets/50812

Changes

adds _executeArrayDepth to track re-entrance to _execute_array

does not call _execute_array when depth >0; instead dispatches directly from PostHog.prototype to bypass the Proxy

will remove the tiktok-proxy playground file before merging, but including it for context / testing

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/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

vercel bot commented Feb 17, 2026

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

Project Deployment Actions Updated (UTC)
posthog-js Ready Ready Preview Feb 17, 2026 10:06pm
posthog-nextjs-config Ready Ready Preview Feb 17, 2026 10:06pm

Request Review

Copy link
Contributor Author

adboio commented Feb 17, 2026

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

@adboio adboio force-pushed the 02-17-fix_sdk_prevent_infinite_recursion_with_proxy branch from aa2a63a to 19de717 Compare February 17, 2026 21:59
@github-actions
Copy link
Contributor

github-actions bot commented Feb 17, 2026

@@ -0,0 +1,263 @@
/* eslint-disable no-console */
Copy link
Contributor Author

Choose a reason for hiding this comment

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

will remove before merging; including for context

@github-actions
Copy link
Contributor

github-actions bot commented Feb 17, 2026

Size Change: +1.15 kB (+0.02%)

Total Size: 6.02 MB

Filename Size Change
packages/browser/dist/array.full.es5.js 321 kB +119 B (+0.04%)
packages/browser/dist/array.full.js 421 kB +116 B (+0.03%)
packages/browser/dist/array.full.no-external.js 441 kB +116 B (+0.03%)
packages/browser/dist/array.js 176 kB +113 B (+0.06%)
packages/browser/dist/array.no-external.js 190 kB +113 B (+0.06%)
packages/browser/dist/main.js 180 kB +113 B (+0.06%)
packages/browser/dist/module.full.js 424 kB +116 B (+0.03%)
packages/browser/dist/module.full.no-external.js 443 kB +116 B (+0.03%)
packages/browser/dist/module.js 179 kB +113 B (+0.06%)
packages/browser/dist/module.no-external.js 194 kB +113 B (+0.06%)
ℹ️ View Unchanged
Filename Size Change
packages/ai/dist/anthropic/index.cjs 19.6 kB 0 B
packages/ai/dist/anthropic/index.mjs 19.2 kB 0 B
packages/ai/dist/gemini/index.cjs 26.5 kB 0 B
packages/ai/dist/gemini/index.mjs 26.3 kB 0 B
packages/ai/dist/index.cjs 160 kB 0 B
packages/ai/dist/index.mjs 159 kB 0 B
packages/ai/dist/langchain/index.cjs 45.3 kB 0 B
packages/ai/dist/langchain/index.mjs 44.8 kB 0 B
packages/ai/dist/openai/index.cjs 46 kB 0 B
packages/ai/dist/openai/index.mjs 45.7 kB 0 B
packages/ai/dist/vercel/index.cjs 34.8 kB 0 B
packages/ai/dist/vercel/index.mjs 34.8 kB 0 B
packages/browser/dist/all-external-dependencies.js 268 kB 0 B
packages/browser/dist/conversations.js 55.5 kB 0 B
packages/browser/dist/crisp-chat-integration.js 2.11 kB 0 B
packages/browser/dist/customizations.full.js 18 kB 0 B
packages/browser/dist/dead-clicks-autocapture.js 13.1 kB 0 B
packages/browser/dist/element-inference.js 5.85 kB 0 B
packages/browser/dist/exception-autocapture.js 11.9 kB 0 B
packages/browser/dist/external-scripts-loader.js 3.04 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/logs.js 39 kB 0 B
packages/browser/dist/posthog-recorder.js 252 kB 0 B
packages/browser/dist/product-tours-preview.js 75.9 kB 0 B
packages/browser/dist/product-tours.js 115 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 76.4 kB 0 B
packages/browser/dist/surveys.js 90 kB 0 B
packages/browser/dist/tracing-headers.js 1.93 kB 0 B
packages/browser/dist/web-vitals-with-attribution.js 12 kB 0 B
packages/browser/dist/web-vitals.js 6.6 kB 0 B
packages/browser/react/dist/esm/index.js 20.5 kB 0 B
packages/browser/react/dist/esm/surveys/index.js 4.54 kB 0 B
packages/browser/react/dist/umd/index.js 23.8 kB 0 B
packages/browser/react/dist/umd/surveys/index.js 5.49 kB 0 B
packages/convex/dist/client/index.js 7.43 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.49 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.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.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 6.87 kB 0 B
packages/core/dist/index.mjs 650 B 0 B
packages/core/dist/posthog-core-stateless.js 31.1 kB 0 B
packages/core/dist/posthog-core-stateless.mjs 28.6 kB 0 B
packages/core/dist/posthog-core.js 40.7 kB 0 B
packages/core/dist/posthog-core.mjs 35.8 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.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 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 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 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 4 kB 0 B
packages/nextjs-config/dist/utils.mjs 1.89 kB 0 B
packages/node/dist/client.js 30 kB 0 B
packages/node/dist/client.mjs 28 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 4.22 kB 0 B
packages/node/dist/exports.mjs 203 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 4.2 kB 0 B
packages/node/dist/extensions/error-tracking/index.mjs 2.91 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 33.3 kB 0 B
packages/node/dist/extensions/feature-flags/feature-flags.mjs 31.3 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 47 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 597 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.16 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 32.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.77 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.27 kB 0 B
packages/react-native/dist/version.js 130 B 0 B
packages/react/dist/esm/index.js 20.5 kB 0 B
packages/react/dist/esm/surveys/index.js 4.54 kB 0 B
packages/react/dist/umd/index.js 23.8 kB 0 B
packages/react/dist/umd/surveys/index.js 5.49 kB 0 B
packages/rollup-plugin/dist/index.js 5.15 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/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 3.15 kB 0 B
packages/webpack-plugin/dist/config.mjs 2.14 kB 0 B
packages/webpack-plugin/dist/index.js 6.53 kB 0 B
packages/webpack-plugin/dist/index.mjs 3.12 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

// Tracks re-entrant calls to _execute_array. Used to detect when a third-party
// Proxy (e.g., TikTok's in-app browser) wraps window.posthog and converts method
// calls into push() calls, which would otherwise cause infinite recursion.
let _executeArrayDepth = 0
Copy link

Choose a reason for hiding this comment

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

Critical bug: _executeArrayDepth is a module-level global variable shared across all PostHog instances. This causes incorrect behavior when multiple instances exist.

Scenario that breaks:

  1. Instance A calls _execute_array()_executeArrayDepth = 1
  2. During execution, Instance B's push() is called normally (no proxy)
  3. Instance B sees _executeArrayDepth > 0 and incorrectly assumes it's a re-entrant call
  4. Instance B bypasses _execute_array() and calls the method directly, skipping the normal queuing/ordering logic

Fix: Move _executeArrayDepth to be an instance variable:

class PostHog {
    private _executeArrayDepth: number = 0
    // ... rest of class
}

Then update references from _executeArrayDepth to this._executeArrayDepth in both _execute_array() and push() methods.

Suggested change
let _executeArrayDepth = 0
// This variable is now moved to be an instance property of the PostHog class

Spotted by Graphite Agent

Fix in Graphite


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

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

4 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +1 to +263
/* eslint-disable no-console */
import { usePostHog } from 'posthog-js/react'
import { useState, useCallback } from 'react'

/**
* Simulates TikTok's in-app browser Proxy behavior.
*
* TikTok's WebView injects a script that wraps `window.posthog` with a
* JavaScript Proxy. The Proxy intercepts known analytics method calls
* (capture, identify, getFeatureFlag, isFeatureEnabled, has_opted_out_capturing,
* etc.) and converts them into `target.push([methodName, ...args])` calls.
*
* This creates an infinite recursion loop:
* _execute_array -> this[method] -> Proxy intercept -> push() ->
* _execute_array -> this[method] -> Proxy intercept -> push() -> ...
*
* The Proxy does NOT intercept internal/private methods like push, _execute_array,
* config, etc. — only well-known analytics API methods.
*/
function wrapWithTikTokProxy(): { interceptCount: number } {
const state = { interceptCount: 0 }

const target = (window as any).posthog
if (!target || target.__tiktokProxied) {
return state
}

// Methods that TikTok's Proxy intercepts (based on the stack traces)
const interceptedMethods = new Set([
'capture',
'identify',
'alias',
'getFeatureFlag',
'isFeatureEnabled',
'getFeatureFlagPayload',
'has_opted_out_capturing',
'opt_out_capturing',
'opt_in_capturing',
'register',
'register_once',
'unregister',
'set_config',
'people',
])

const proxy = new Proxy(target, {
get(obj, prop, receiver) {
const value = Reflect.get(obj, prop, receiver)

// Only intercept known analytics methods
if (typeof prop === 'string' && interceptedMethods.has(prop) && typeof value === 'function') {
return function (this: any, ...args: any[]) {
state.interceptCount++
console.log(`[TikTok Proxy] Intercepted: ${prop}(`, ...args, ')')
// This is what TikTok's Proxy does: convert the method call
// into a push() call, mimicking the pre-load snippet behavior
proxy.push([prop].concat(Array.prototype.slice.call(args, 0)))
}
}

return value
},
})

proxy.__tiktokProxied = true
;(window as any).posthog = proxy

return state
}

export default function TikTokProxyPage() {
const posthog = usePostHog()
const [log, setLog] = useState<string[]>([])
const [proxyActive, setProxyActive] = useState(false)
const [interceptCount, setInterceptCount] = useState(0)
const [proxyState, setProxyState] = useState<{ interceptCount: number } | null>(null)

const addLog = useCallback((msg: string) => {
setLog((prev) => [...prev, `[${new Date().toISOString().split('T')[1].split('.')[0]}] ${msg}`])
}, [])

const enableProxy = useCallback(() => {
const state = wrapWithTikTokProxy()
setProxyState(state)
setProxyActive(true)
addLog('TikTok Proxy enabled — window.posthog is now wrapped')
}, [addLog])

const disableProxy = useCallback(() => {
if ((window as any).posthog?.__tiktokProxied) {
// Restore the original posthog instance
;(window as any).posthog = posthog
setProxyActive(false)
addLog('TikTok Proxy disabled — window.posthog restored')
}
}, [posthog, addLog])

const refreshCount = useCallback(() => {
if (proxyState) {
setInterceptCount(proxyState.interceptCount)
}
}, [proxyState])

const testCapture = useCallback(() => {
try {
addLog('Calling window.posthog.capture("test-tiktok-event")...')
;(window as any).posthog.capture('test-tiktok-event', { source: 'tiktok-proxy-test' })
addLog('capture() completed without error')
} catch (e: any) {
addLog(`ERROR: ${e.name}: ${e.message}`)
}
refreshCount()
}, [addLog, refreshCount])

const testGetFeatureFlag = useCallback(() => {
try {
addLog('Calling window.posthog.getFeatureFlag("test-flag")...')
const result = (window as any).posthog.getFeatureFlag('test-flag')
addLog(`getFeatureFlag() returned: ${JSON.stringify(result)}`)
} catch (e: any) {
addLog(`ERROR: ${e.name}: ${e.message}`)
}
refreshCount()
}, [addLog, refreshCount])

const testIsFeatureEnabled = useCallback(() => {
try {
addLog('Calling window.posthog.isFeatureEnabled("test-flag")...')
const result = (window as any).posthog.isFeatureEnabled('test-flag')
addLog(`isFeatureEnabled() returned: ${JSON.stringify(result)}`)
} catch (e: any) {
addLog(`ERROR: ${e.name}: ${e.message}`)
}
refreshCount()
}, [addLog, refreshCount])

const testHasOptedOut = useCallback(() => {
try {
addLog('Calling window.posthog.has_opted_out_capturing()...')
const result = (window as any).posthog.has_opted_out_capturing()
addLog(`has_opted_out_capturing() returned: ${JSON.stringify(result)}`)
} catch (e: any) {
addLog(`ERROR: ${e.name}: ${e.message}`)
}
refreshCount()
}, [addLog, refreshCount])

const testAllMethods = useCallback(() => {
addLog('--- Running all method tests ---')
testCapture()
testGetFeatureFlag()
testIsFeatureEnabled()
testHasOptedOut()
addLog('--- All tests complete ---')
}, [addLog, testCapture, testGetFeatureFlag, testIsFeatureEnabled, testHasOptedOut])

return (
<div style={{ padding: 20, fontFamily: 'system-ui, sans-serif' }}>
<h1>TikTok In-App Browser Proxy Reproduction</h1>
<p style={{ color: '#666', maxWidth: 700 }}>
This page simulates the behavior of TikTok&apos;s in-app browser, which wraps{' '}
<code>window.posthog</code> with a JavaScript Proxy that converts method calls into <code>push()</code>{' '}
calls, causing infinite recursion.
</p>

<div
style={{
padding: 12,
marginBottom: 16,
borderRadius: 6,
background: proxyActive ? '#fee2e2' : '#f0fdf4',
border: `1px solid ${proxyActive ? '#fca5a5' : '#86efac'}`,
}}
>
<strong>Proxy Status:</strong> {proxyActive ? 'ACTIVE' : 'Inactive'}
{proxyActive && ` — ${interceptCount} interceptions`}
</div>

<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 16 }}>
<button
onClick={enableProxy}
disabled={proxyActive}
style={{
padding: '8px 16px',
background: proxyActive ? '#ccc' : '#ef4444',
color: 'white',
border: 'none',
borderRadius: 4,
cursor: proxyActive ? 'not-allowed' : 'pointer',
}}
>
Enable TikTok Proxy
</button>
<button
onClick={disableProxy}
disabled={!proxyActive}
style={{
padding: '8px 16px',
background: !proxyActive ? '#ccc' : '#22c55e',
color: 'white',
border: 'none',
borderRadius: 4,
cursor: !proxyActive ? 'not-allowed' : 'pointer',
}}
>
Disable Proxy
</button>
</div>

<h2>Test Methods</h2>
<p style={{ color: '#666' }}>
Enable the proxy above, then click these buttons. Without the fix, these will throw{' '}
<code>RangeError: Maximum call stack size exceeded</code>.
</p>

<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 16 }}>
<button onClick={testCapture} style={{ padding: '8px 16px' }}>
capture()
</button>
<button onClick={testGetFeatureFlag} style={{ padding: '8px 16px' }}>
getFeatureFlag()
</button>
<button onClick={testIsFeatureEnabled} style={{ padding: '8px 16px' }}>
isFeatureEnabled()
</button>
<button onClick={testHasOptedOut} style={{ padding: '8px 16px' }}>
has_opted_out_capturing()
</button>
<button
onClick={testAllMethods}
style={{
padding: '8px 16px',
background: '#3b82f6',
color: 'white',
border: 'none',
borderRadius: 4,
}}
>
Run All
</button>
</div>

<h2>Log</h2>
<button onClick={() => setLog([])} style={{ marginBottom: 8, padding: '4px 12px' }}>
Clear
</button>
<pre
style={{
background: '#1e1e1e',
color: '#d4d4d4',
padding: 16,
borderRadius: 6,
maxHeight: 400,
overflow: 'auto',
fontSize: 13,
lineHeight: 1.5,
}}
>
{log.length === 0 ? '(no log entries yet)' : log.join('\n')}
</pre>
</div>
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

the PR description says this playground file should be removed before merging - it's still included in the commit

Prompt To Fix With AI
This is a comment left during a code review.
Path: playground/nextjs/pages/tiktok-proxy.tsx
Line: 1:263

Comment:
the PR description says this playground file should be removed before merging - it's still included in the commit

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

Copy link
Member

@rafaeelaudibert rafaeelaudibert left a comment

Choose a reason for hiding this comment

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

This is insane. Any idea why they'd do it? I tried fixing it but could never repro, amazing job here!

Also, keep the example, that's helpful!

@ioannisj
Copy link
Contributor

I'd imagine this isn't unique to tiktok. 👍

@charlesvien
Copy link
Member

in-app browsers 😕

Copy link
Contributor Author

adboio commented Feb 18, 2026

@rafaeelaudibert ty for review!

anything in the pursuit of more data i guess lol - not 100% sure tho, if i have time i'm gonna try to repro actually in-tiktok-browser and add some logging to figure out what the proxy is doing

@adboio adboio merged commit c0b911d into main Feb 18, 2026
50 checks passed
@adboio adboio deleted the 02-17-fix_sdk_prevent_infinite_recursion_with_proxy branch February 18, 2026 16:08
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.

4 participants