Files
zeekay df17f47052 Call the product endpoints under /v1
insights-web serves surveys, early access features, web experiments,
product tours and the conversations widget at /v1/ and answers /api/
with 404, so every one of these calls failed.

Claude-Session: https://claude.ai/code/session_011i2AG9nwpiKUCVxAdDzYuU
2026-09-29 18:49:25 -07:00

393 lines
15 KiB
TypeScript

import { test, expect } from './utils/insights-playwright-test-base'
import { Compression, FlagsResponse, InsightsConfig } from '@/types'
import { Insights } from '@/insights-core'
/**
* Regression test for https://github.com/Insights/insights-js/issues/3313
*
* When users import the slim bundle (`insights-js/dist/module.slim`) together with
* extension bundles (`insights-js/dist/extension-bundles`), property mangling can
* cause crashes because the two files are compiled as separate rollup entries and
* terser may mangle `_`-prefixed properties to different names in each bundle.
*
* For example, `_internalEventEmitter` might be mangled to `ti` in extension-bundles
* but `oe` in module.slim, so `InsightsFeatureFlags.reloadFeatureFlags()` crashes with:
* TypeError: Cannot read properties of undefined (reading 'emit')
*/
const SLIM_BUNDLE_URL = '/playground/slim-bundle/index.html'
/** Helper: wait for ES modules on the page to finish loading. */
async function waitForSlimBundleReady(page: import('@playwright/test').Page) {
await page.waitForFunction(() => (window as any).__slim_bundle_ready === true, null, { timeout: 5000 })
}
/** Helper: init Insights on the page with the given extension bundle(s). */
async function initInsightsWithExtensions(
page: import('@playwright/test').Page,
extensionVarName: string,
extraConfig: Record<string, any> = {}
) {
return page.evaluate(
([extName, extra]) => {
try {
const ph = (window as any).insights as Insights
const extensions = (window as any)[extName]
ph.init('test-token', {
api_host: 'https://localhost:1234',
debug: true,
ip: false,
capture_pageview: false,
__extensionClasses: { ...extensions },
opt_out_useragent_filter: true,
...extra,
} as Partial<InsightsConfig>)
return null
} catch (e: any) {
return e.message
}
},
[extensionVarName, extraConfig] as const
)
}
test.describe('slim bundle + extension bundles (#3313)', () => {
test.beforeEach(async ({ context }) => {
// Mock the remote config endpoint
void context.route(/\/array\/[^/]+\/config(\?|$)/, (route) => {
const flagsResponse: FlagsResponse = {
editorParams: {},
flags: {},
featureFlags: {},
featureFlagPayloads: {},
errorsWhileComputingFlags: false,
toolbarParams: {},
toolbarVersion: 'toolbar',
isAuthenticated: false,
siteApps: [],
supportedCompression: [Compression.GZipJS],
autocaptureExceptions: false,
}
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(flagsResponse),
})
})
// Mock the flags endpoint
void context.route('**/flags/*', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
featureFlags: { 'test-flag': true },
featureFlagPayloads: {},
errorsWhileComputingFlags: false,
}),
})
})
// Mock the capture endpoint
void context.route('**/e/*', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ status: 1 }),
})
})
// Mock the surveys endpoint
void context.route('**/v1/surveys/*', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ surveys: [] }),
})
})
})
// ── FeatureFlagsExtensions ──────────────────────────────────────────
test('FeatureFlagsExtensions: init does not crash', async ({ page }) => {
const errors: string[] = []
page.on('pageerror', (error) => errors.push(error.message))
await page.goto(SLIM_BUNDLE_URL)
await waitForSlimBundleReady(page)
const initError = await initInsightsWithExtensions(page, 'FeatureFlagsExtensions')
await page.waitForTimeout(1000)
expect(initError).toBeNull()
expect(errors).toEqual([])
})
test('FeatureFlagsExtensions: reloadFeatureFlags does not crash', async ({ page }) => {
const errors: string[] = []
page.on('pageerror', (error) => errors.push(error.message))
await page.goto(SLIM_BUNDLE_URL)
await waitForSlimBundleReady(page)
const error = await page.evaluate(() => {
try {
const ph = (window as any).insights as Insights
const extensions = (window as any).FeatureFlagsExtensions
ph.init('test-token', {
api_host: 'https://localhost:1234',
debug: true,
ip: false,
capture_pageview: false,
__extensionClasses: { ...extensions },
opt_out_useragent_filter: true,
} as Partial<InsightsConfig>)
ph.reloadFeatureFlags()
return null
} catch (e: any) {
return e.message
}
})
await page.waitForTimeout(1000)
expect(error).toBeNull()
expect(errors).toEqual([])
})
test('FeatureFlagsExtensions: getFeatureFlag works with bootstrapped flags', async ({ page }) => {
const errors: string[] = []
page.on('pageerror', (error) => errors.push(error.message))
await page.goto(SLIM_BUNDLE_URL)
await waitForSlimBundleReady(page)
const flagValue = await page.evaluate(() => {
try {
const ph = (window as any).insights as Insights
const extensions = (window as any).FeatureFlagsExtensions
ph.init('test-token', {
api_host: 'https://localhost:1234',
debug: true,
ip: false,
capture_pageview: false,
__extensionClasses: { ...extensions },
opt_out_useragent_filter: true,
bootstrap: { featureFlags: { 'test-flag': true } },
} as Partial<InsightsConfig>)
return { value: ph.getFeatureFlag('test-flag'), error: null }
} catch (e: any) {
return { value: null, error: e.message }
}
})
expect(flagValue.error).toBeNull()
expect(flagValue.value).toBe(true)
expect(errors).toEqual([])
})
// ── ErrorTrackingExtensions ─────────────────────────────────────────
test('ErrorTrackingExtensions: captureException does not crash', async ({ page }) => {
const errors: string[] = []
page.on('pageerror', (error) => errors.push(error.message))
await page.goto(SLIM_BUNDLE_URL)
await waitForSlimBundleReady(page)
const error = await page.evaluate(() => {
try {
const ph = (window as any).insights as Insights
const extensions = (window as any).ErrorTrackingExtensions
ph.init('test-token', {
api_host: 'https://localhost:1234',
debug: true,
ip: false,
capture_pageview: false,
__extensionClasses: { ...extensions },
opt_out_useragent_filter: true,
} as Partial<InsightsConfig>)
ph.captureException(new Error('test error'), { extra: 'data' })
return null
} catch (e: any) {
return e.message
}
})
await page.waitForTimeout(1000)
expect(error).toBeNull()
expect(errors).toEqual([])
})
// ── ToolbarExtensions ───────────────────────────────────────────────
test('ToolbarExtensions: loadToolbar does not crash', async ({ page }) => {
const errors: string[] = []
page.on('pageerror', (error) => errors.push(error.message))
await page.goto(SLIM_BUNDLE_URL)
await waitForSlimBundleReady(page)
const error = await page.evaluate(() => {
try {
const ph = (window as any).insights as Insights
const extensions = (window as any).ToolbarExtensions
ph.init('test-token', {
api_host: 'https://localhost:1234',
debug: true,
ip: false,
capture_pageview: false,
__extensionClasses: { ...extensions },
opt_out_useragent_filter: true,
} as Partial<InsightsConfig>)
// loadToolbar returns false when there are no toolbar params — that's fine,
// we just want to make sure it doesn't throw.
ph.loadToolbar({})
return null
} catch (e: any) {
return e.message
}
})
await page.waitForTimeout(1000)
expect(error).toBeNull()
expect(errors).toEqual([])
})
// ── SurveysExtensions ───────────────────────────────────────────────
test('SurveysExtensions: getSurveys does not crash', async ({ page }) => {
const errors: string[] = []
page.on('pageerror', (error) => errors.push(error.message))
await page.goto(SLIM_BUNDLE_URL)
await waitForSlimBundleReady(page)
const error = await page.evaluate(() => {
return new Promise<string | null>((resolve) => {
try {
const ph = (window as any).insights as Insights
const extensions = (window as any).SurveysExtensions
ph.init('test-token', {
api_host: 'https://localhost:1234',
debug: true,
ip: false,
capture_pageview: false,
__extensionClasses: { ...extensions },
opt_out_useragent_filter: true,
} as Partial<InsightsConfig>)
ph.getSurveys(() => {
resolve(null)
})
} catch (e: any) {
resolve(e.message)
}
})
})
await page.waitForTimeout(1000)
expect(error).toBeNull()
expect(errors).toEqual([])
})
// ── AnalyticsExtensions (Autocapture) ─────────────────────────────
test('AnalyticsExtensions: autocapture init does not crash', async ({ page }) => {
// Autocapture accesses this.instance._shouldDisableFlags() which is mangled
const errors: string[] = []
page.on('pageerror', (error) => errors.push(error.message))
await page.goto(SLIM_BUNDLE_URL)
await waitForSlimBundleReady(page)
const error = await page.evaluate(() => {
try {
const ph = (window as any).insights as Insights
const extensions = (window as any).AnalyticsExtensions
ph.init('test-token', {
api_host: 'https://localhost:1234',
debug: true,
ip: false,
capture_pageview: false,
autocapture: true,
__extensionClasses: { ...extensions },
opt_out_useragent_filter: true,
} as Partial<InsightsConfig>)
return null
} catch (e: any) {
return e.message
}
})
await page.waitForTimeout(1000)
expect(error).toBeNull()
expect(errors).toEqual([])
})
// ── Every extension bundle: init does not crash ───────────────────
for (const extName of [
'SiteAppsExtensions',
'SessionReplayExtensions',
'ExperimentsExtensions',
'ConversationsExtensions',
'LogsExtensions',
'ProductToursExtensions',
'TracingExtensions',
] as const) {
test(`${extName}: init does not crash`, async ({ page }) => {
const errors: string[] = []
page.on('pageerror', (error) => errors.push(error.message))
await page.goto(SLIM_BUNDLE_URL)
await waitForSlimBundleReady(page)
const error = await initInsightsWithExtensions(page, extName)
await page.waitForTimeout(1000)
expect(error).toBeNull()
expect(errors).toEqual([])
})
}
// ── AllExtensions ───────────────────────────────────────────────────
test('AllExtensions: init + multiple features do not crash', async ({ page }) => {
const errors: string[] = []
page.on('pageerror', (error) => errors.push(error.message))
await page.goto(SLIM_BUNDLE_URL)
await waitForSlimBundleReady(page)
const result = await page.evaluate(() => {
try {
const ph = (window as any).insights as Insights
const extensions = (window as any).AllExtensions
ph.init('test-token', {
api_host: 'https://localhost:1234',
debug: true,
ip: false,
capture_pageview: false,
__extensionClasses: { ...extensions },
opt_out_useragent_filter: true,
bootstrap: { featureFlags: { 'test-flag': 'variant-a' } },
} as Partial<InsightsConfig>)
// Exercise multiple TreeShakeable<T> code paths in one test:
const flagValue = ph.getFeatureFlag('test-flag')
ph.reloadFeatureFlags()
ph.captureException(new Error('test error'))
ph.loadToolbar({})
return { flagValue, error: null }
} catch (e: any) {
return { flagValue: null, error: e.message }
}
})
await page.waitForTimeout(1000)
expect(result.error).toBeNull()
expect(result.flagValue).toBe('variant-a')
expect(errors).toEqual([])
})
})