Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions frontend/src/toolbar/shims/posthogTyped.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { CaptureOptions, CaptureResult, Properties } from 'posthog-js'
import originalPostHog from 'posthog-js'

// Toolbar shim — lib/posthog-typed is ~200 KB of generated event typings wrapped around a
// small runtime. This mirrors that runtime exactly (capture/captureRaw delegating to the
// posthog-js singleton, everything else proxied through) without the typings.
const enhanced: Record<string, unknown> = {
capture: (
event_name: string,
properties?: Properties | null,
options?: CaptureOptions
): CaptureResult | undefined => originalPostHog.capture(event_name, properties, options),
captureRaw: (
event_name: string,
properties?: Properties | null,
options?: CaptureOptions
): CaptureResult | undefined => originalPostHog.capture(event_name, properties, options),
}

const posthog = new Proxy(enhanced, {
get(target, prop) {
if (prop in target) {
return target[prop as string]
}
return (originalPostHog as unknown as Record<string | symbol, unknown>)[prop]
},
set(_target, prop, value) {
;(originalPostHog as unknown as Record<string | symbol, unknown>)[prop] = value
return true
},
}) as unknown as typeof originalPostHog & { captureRaw: typeof originalPostHog.capture }

export default posthog

// Re-export everything else from posthog-js, matching lib/posthog-typed's surface
export * from 'posthog-js'
22 changes: 22 additions & 0 deletions frontend/src/toolbar/shims/shims.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { expectLogic } from 'kea-test-utils'
import posthogJs from 'posthog-js'

import typedPosthog from 'lib/posthog-typed'

import { initKeaTests } from '~/test/init'

import { featureFlagLogic, getFeatureFlagPayload } from './featureFlagLogic'
import { membersLogic } from './membersLogic'
import shimmedPosthog from './posthogTyped'
import { sceneLogic } from './sceneLogic'
import { surveyQuestionLabelsLogic } from './surveyQuestionLabelsLogic'
import { isAuthenticatedTeam, teamLogic } from './teamLogic'
Expand Down Expand Up @@ -76,4 +80,22 @@ describe('toolbar shims', () => {
expect(getFeatureFlagPayload(flag)).toBeUndefined()
})
})

describe('posthogTyped shim', () => {
it('exposes every runtime member lib/posthog-typed adds beyond posthog-js', () => {
// The toolbar build swaps lib/posthog-typed for the shim, invisible to the
// typechecker — if the generator grows a new runtime method, the shim must too,
// or toolbar code calling it crashes on customer pages.
for (const key of Object.keys(typedPosthog)) {
expect(typeof (shimmedPosthog as any)[key]).toBe(typeof (typedPosthog as any)[key])
}
})

it.each([['capture'], ['captureRaw']] as const)('%s delegates to posthog-js capture', (method) => {
const captureSpy = jest.spyOn(posthogJs, 'capture').mockReturnValue(undefined)
shimmedPosthog[method]('some event', { foo: 'bar' })
expect(captureSpy).toHaveBeenCalledWith('some event', { foo: 'bar' }, undefined)
captureSpy.mockRestore()
})
})
})
25 changes: 25 additions & 0 deletions frontend/toolbar-config.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import * as fs from 'fs'
import { createRequire } from 'module'
import * as path from 'path'

import { commonConfig, copyRRWebWorkerFiles, createHashlessEntrypoints, esbuildBuild, isDev } from '@posthog/esbuilder'

const require = createRequire(import.meta.url)

// `TOOLBAR_PUBLIC_PATH`, when set, overrides the default `publicPath` so the
// toolbar bundle and its assets can be hosted under a versioned, content-pinned
// URL on the posthog-js CDN. Used by posthog-js's release workflow to ship a
Expand Down Expand Up @@ -34,6 +37,9 @@ const shimmedModules = {
// app (TZLabel, Link, HeatmapEventsPanel), whose scenes/urls import would otherwise pull
// every product manifest into the bundle.
'scenes/urls': 'src/toolbar/urls.ts',
// Not a kea shim: the generated event typings are ~200 KB of source the toolbar must
// not bundle. The shim mirrors the module's small runtime (capture/captureRaw proxy).
'lib/posthog-typed': 'src/toolbar/shims/posthogTyped.ts',
}

// Modules replaced with an inert proxy that logs access in debug mode
Expand Down Expand Up @@ -80,9 +86,28 @@ function createToolbarModulePlugin(dirname) {
path.resolve(dirname, shimFile),
])
)
// lowlight's package index re-exports `all` — every highlight.js grammar, ~1.2 MB of
// source — alongside the `common` set our code actually uses. The re-export alone pulls
// all 192 grammars into the bundle graph, so resolve 'lowlight' to a pared-down module
// instead. Its relative imports bypass the package's exports map (which hides lib/).
const lowlightDir = path.dirname(require.resolve('lowlight'))
Comment on lines +89 to +93

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.

The best question here is: why is lowlight being pulled here at all? Can we simply load lowlight lazily wherever it's being loaded? This way it will not be added to the same bundle as the toolbar

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good news: lowlight is already lazy. The product tours code loads it through a dynamic import, so esbuild splits the whole tiptap/lowlight chain into its own chunk (~662 KB) that only downloads when someone actually saves or previews a tour step in the toolbar. It never ships on page load.

I also measured what this PR actually saves in shipped bytes: 90 bytes eager, 0 deferred. The entire 1.33 MB "win" was in the pre-tree-shake input metric, which is exactly what #72583 deletes. Tree shaking was already throwing all of this away.

So this PR trades a shim, a parity test, and a build hack for 90 bytes. Not worth it. Closing in favor of #72583.


return {
name: 'toolbar-module-replacements',
setup(build) {
build.onResolve({ filter: /^lowlight$/ }, () => ({
path: 'lowlight',
namespace: 'lowlight-common-only',
}))
build.onLoad({ filter: /.*/, namespace: 'lowlight-common-only' }, () => ({
contents: `
export { createLowlight } from './lib/index.js'
export { grammars as common } from './lib/common.js'
`,
resolveDir: lowlightDir,
loader: 'js',
}))

build.onResolve({ filter: /.*/ }, (args) => {
const shimFile = shimmedModules[args.path] ?? shimmedModules[args.path.replace(/^~\//, '')]
if (shimFile) {
Expand Down
Loading