mirror of
https://github.com/hanzoai/insights-js.git
synced 2026-11-02 09:24:43 +00:00
* feat: Make RemoteConfig the sole config loading mechanism Remove the __preview_remote_config gate, the legacy flags() method, and the _flagsCalled pattern. RemoteConfig is now always used to load SDK config, with a 5-minute visibility-aware refresh interval for flags. Key changes: - remote-config.ts: Add refresh interval with visibility-aware polling, refresh() only reloads flags (not full pipeline) to avoid SiteApps duplicate listeners, add stop() for cleanup - posthog-featureflags.ts: Remove flags() method and _flagsCalled, always use /flags/?v=2, always include timezone - posthog-core.ts: Store RemoteConfigLoader instance, call stop() on reset(), fix process_person config alias preservation - posthog-config.ts: Remove __preview_remote_config type Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: Handle remote config failure gracefully and fix CI When the remote config endpoint fails, pass an empty config to _onRemoteConfig so extensions (autocapture, etc.) still initialize with defaults. Also fix terser-mangled-names.json alphabetical ordering, remove remaining config=true references from flags URLs, and add config endpoint mock for Playwright tests. * fix: Mock remote config endpoint in Playwright test infrastructure Add config endpoint mock to both setup.ts and network.ts fixtures so that RemoteConfig (now the sole config mechanism) loads properly in Playwright mocked tests. Uses the same flagsResponse data to ensure test-specific overrides (surveys, session recording, etc.) are included in the config. * fix: Update Playwright tests for RemoteConfig-first request flow - flags.spec.ts: Add timezone to expected flags payload - hydration-error.spec.ts: Add config endpoint mock so session recording loads properly - network-recorder.spec.ts: Update expected capture order - config JSON fetch now precedes flags fetch Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: Stabilize tests for RemoteConfig-first loading - Replace jest.runAllTimers() with advanceTimersByTime(10) in $feature_flag_error tracking tests to avoid infinite loop from RemoteConfigLoader's 5-minute setInterval - Use set-based assertions in network-recorder Playwright tests to handle non-deterministic request ordering and flaky config.js entries - Add 200ms settling wait before mouse move tests to prevent premature buffer flushes from async flag loading Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: Remove unreliable flags fetch assertion from network recorder test The /flags/ fetch doesn't reliably appear in Performance Resource Timing entries in CI chromium due to debounced timing. This assertion is not essential - the test's purpose is verifying the network recorder captures user-initiated fetch/xhr requests to example.com. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: Prevent empty remote config from overwriting persisted feature settings When remote config fetch fails and returns an empty object {}, several consumers were blindly writing "disabled" defaults to persistence, overwriting previously valid server config. This could degrade a returning user's experience on the next page load. Add guards to check for key existence before writing to persistence: - heatmaps: check for 'heatmaps' key - product tours: check for 'productTours' key - dead clicks: check for 'captureDeadClicks' key - web vitals: check for 'capturePerformance' key - error tracking: check for 'errorTracking' key This matches the pattern already used by session recording. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix: Address PR review feedback for remote config handling 1. Add guards to exception-autocapture and autocapture onRemoteConfig methods to prevent overwriting persisted values when config fetch fails: - exception-autocapture: check for 'autocaptureExceptions' key - autocapture: check for 'autocapture_opt_out' key 2. Simplify visibility handling in RemoteConfigLoader: - Remove event listener and timestamp tracking complexity - Check visibility directly in refresh() method - Interval runs continuously, refresh() is a no-op when tab is hidden - Eliminates handler, event listener, and timestamp tracking 3. Document the 5-minute refresh interval choice: - Balances flag freshness with server load - Most sessions are shorter than 5 minutes anyway 4. Update test helper to provide minimal remote config with autocapture_opt_out so extensions initialize properly in tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * docs: Add comment explaining autocapture's intentional opt-out model Clarifies why autocapture doesn't have a guard against missing autocapture_opt_out key, unlike other extensions. This prevents future confusion during code reviews. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * include changeset --------- Co-authored-by: dylan <[email protected]> Co-authored-by: Claude Opus 4.6 <[email protected]>
166 lines
5.3 KiB
TypeScript
166 lines
5.3 KiB
TypeScript
import { expect, Page, Response } from '@playwright/test'
|
|
import fs from 'fs'
|
|
import path from 'path'
|
|
import { Compression, FlagsResponse } from '@/types'
|
|
import { testPage } from './page'
|
|
|
|
// read directory ../../dist and get all files
|
|
const files = fs.readdirSync(path.join(__dirname, '../../dist'))
|
|
|
|
export const testNetwork = testPage.extend<{
|
|
network: NetworkPage
|
|
mockIngestion: boolean
|
|
flagsOverrides: Partial<FlagsResponse>
|
|
staticOverrides: Record<string, string>
|
|
}>({
|
|
staticOverrides: [{}, { option: true }],
|
|
flagsOverrides: [
|
|
{
|
|
sessionRecording: undefined,
|
|
isAuthenticated: false,
|
|
capturePerformance: true,
|
|
},
|
|
{ option: true },
|
|
],
|
|
mockIngestion: true,
|
|
network: [
|
|
async ({ page, flagsOverrides, mockIngestion, staticOverrides }, use) => {
|
|
const networkPage = new NetworkPage(page)
|
|
await networkPage.mockStatic(staticOverrides)
|
|
if (flagsOverrides) {
|
|
await networkPage.mockFlags(flagsOverrides)
|
|
}
|
|
if (mockIngestion) {
|
|
await networkPage.mockIngestion()
|
|
}
|
|
await use(networkPage)
|
|
networkPage.expectNoFailed()
|
|
},
|
|
{ auto: true },
|
|
],
|
|
})
|
|
|
|
export class NetworkPage {
|
|
responses: Response[] = []
|
|
|
|
constructor(private page: Page) {
|
|
page.on('response', (res: Response) => {
|
|
this.responses.push(res)
|
|
})
|
|
}
|
|
|
|
async mockFlags(flagsOverrides: Partial<FlagsResponse>) {
|
|
// Prepare the mocked Flags API response
|
|
const flagsResponse: FlagsResponse = {
|
|
editorParams: {},
|
|
flags: {
|
|
'session-recording-player': {
|
|
key: '7569-insight-cohorts',
|
|
enabled: true,
|
|
variant: undefined,
|
|
reason: {
|
|
code: 'condition_match',
|
|
condition_index: 0,
|
|
description: 'Matched condition set 1',
|
|
},
|
|
metadata: {
|
|
id: 1421,
|
|
version: 1,
|
|
description: undefined,
|
|
payload: undefined,
|
|
},
|
|
},
|
|
},
|
|
featureFlags: { 'session-recording-player': true },
|
|
featureFlagPayloads: {},
|
|
errorsWhileComputingFlags: false,
|
|
toolbarParams: {},
|
|
toolbarVersion: 'toolbar',
|
|
isAuthenticated: false,
|
|
siteApps: [],
|
|
supportedCompression: [Compression.GZipJS],
|
|
autocaptureExceptions: false,
|
|
...flagsOverrides,
|
|
}
|
|
|
|
await this.page.route('**/flags/*', async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(flagsResponse),
|
|
})
|
|
})
|
|
|
|
// Mock the remote config endpoint to return the same config data.
|
|
// RemoteConfig is now the sole config loading mechanism.
|
|
await this.page.route(/\/array\/[^/]+\/config(\?|$)/, async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(flagsResponse),
|
|
})
|
|
})
|
|
}
|
|
|
|
async mockSurveys(surveysResponse: any[]) {
|
|
await this.page.route('**/surveys/**', async (route) => {
|
|
await route.fulfill({
|
|
json: {
|
|
surveys: surveysResponse,
|
|
},
|
|
})
|
|
})
|
|
}
|
|
|
|
async mockIngestion() {
|
|
await this.page.route('**/e/**', async (route) => {
|
|
await route.fulfill({
|
|
headers: { loaded: 'mock captured' },
|
|
json: {},
|
|
})
|
|
})
|
|
}
|
|
|
|
async waitForSurveys() {
|
|
await this.page.waitForResponse('**/surveys/**')
|
|
}
|
|
|
|
async mockStatic(staticOverrides: Record<string, string | undefined>) {
|
|
await Promise.all(
|
|
files.map((file) => {
|
|
return this.page.route(`**/static/${file}*`, async (route) => {
|
|
const source = staticOverrides[file] ?? file
|
|
await route.fulfill({
|
|
headers: { loaded: 'using relative path by playwright', source: source },
|
|
path: `./dist/${source}`,
|
|
})
|
|
})
|
|
})
|
|
)
|
|
}
|
|
|
|
expectNoFailed(): void {
|
|
expect(this.responses.filter((response) => !response.ok)).toHaveLength(0)
|
|
}
|
|
|
|
/**
|
|
* Runs the provided action, waiting for the network requests matching the provided url patterns to complete.
|
|
* Intended when running an action causes network requests that need to complete before we should continue.
|
|
*/
|
|
async waitingForNetworkCausedBy(options: {
|
|
urlPatternsToWaitFor: (string | RegExp)[]
|
|
action: () => Promise<void>
|
|
}): Promise<void> {
|
|
const responsePromises = options.urlPatternsToWaitFor.map((urlPattern) => {
|
|
return this.page.waitForResponse(urlPattern)
|
|
})
|
|
await options.action()
|
|
// eslint-disable-next-line compat/compat
|
|
await Promise.allSettled(responsePromises)
|
|
}
|
|
|
|
async waitForFlags() {
|
|
await this.page.waitForResponse(/flags/)
|
|
}
|
|
}
|