Files
zeekay 26a9531f7b merge: upstream/main (sync, brand preserved)
# Conflicts:
#	.github/workflows/check-insights-major-version.yml
#	.github/workflows/check-posthog-major-version.yml
#	.github/workflows/ci-security.yaml
#	.github/workflows/validate-versioning.yml
#	AGENTS.md
#	AGENTS.md~upstream_main
#	packages/ai/src/otel/capture.ts
#	packages/ai/src/otel/mappers/aiSdk.ts
#	packages/ai/src/otel/mappers/index.ts
#	packages/ai/src/otel/types.ts
#	packages/core/src/__tests__/posthog.init.spec.ts
#	packages/react-native/src/PostHogProvider.tsx
#	packages/react/src/context/PostHogContext.ts
#	packages/react/src/context/PostHogProvider.tsx
#	tooling/eslint-plugin-insights/no-unsafe-web-global.js
2026-06-02 10:52:56 -07:00

254 lines
8.3 KiB
JavaScript

import * as fs from 'fs'
import path from 'path'
import { ClientFunction, RequestLogger, RequestMock } from 'testcafe'
import fetch from 'node-fetch'
// NOTE: These tests are run against a dedicated test project in Insights cloud
// but can be overridden to call a local API when running locally
// User admin for the test project: https://us.insights.com/admin/insights/organization/0182397e-3df4-0000-52e3-d890b5a16955/change/
const currentEnv = process.env
export const {
INSIGHTS_PROJECT_API_KEY,
INSIGHTS_PERSONAL_API_KEY,
INSIGHTS_API_HOST = 'https://us.i.insights.com',
INSIGHTS_PROJECT_ID = '11213',
BRANCH_NAME,
RUN_ID,
BROWSER,
} = currentEnv
const HEADERS = { Authorization: `Bearer ${INSIGHTS_PERSONAL_API_KEY}` }
export const captureLogger = RequestLogger(/ip=0/, {
logRequestHeaders: true,
logRequestBody: true,
logResponseHeaders: true,
logResponseBody: true,
stringifyRequestBody: true,
stringifyResponseBody: true,
})
export const staticFilesMock = RequestMock()
.onRequestTo(/array.full.js/)
.respond((req, res) => {
const ENV_BROWSER = process.env.BROWSER
const fileToRead = ENV_BROWSER === 'browserstack:ie' ? '../dist/array.full.es5.js' : '../dist/array.full.js'
const arrayjs = fs.readFileSync(path.resolve(__dirname, fileToRead))
res.setBody(arrayjs)
})
.onRequestTo(/playground/)
.respond((req, res) => {
const html = fs.readFileSync(path.resolve(__dirname, '../playground/cypress-full/index.html'))
res.setBody(html)
})
export const initInsights = (testName, config) => {
let testSessionId = Math.round(Math.random() * 10000000000).toString()
log(`Initializing insights with testSessionId "${testSessionId}"`)
const insightsConfig = {
...config,
debug: true,
api_host: INSIGHTS_API_HOST,
api_key: INSIGHTS_PROJECT_API_KEY,
bootstrap: {
distinctID: 'automated-tester', // We set this to get around the ingestion delay for new distinctIDs
isIdentifiedID: true,
},
opt_out_useragent_filter: true,
request_batching: false,
}
const register = {
testSessionId,
testName,
testBranchName: BRANCH_NAME,
testRunId: RUN_ID,
testBrowser: BROWSER,
}
return ClientFunction(
(clientInsightsConfig = {}) => {
clientInsightsConfig.loaded = () => {
window.loaded = true
window.fullCaptures = []
}
clientInsightsConfig.before_send = (event) => {
window.fullCaptures.push(event)
return event
}
window.insights.init(clientInsightsConfig.api_key, clientInsightsConfig)
window.hi.register(register)
return testSessionId
},
{
dependencies: {
register,
testSessionId,
},
}
)(insightsConfig)
}
export const isLoaded = ClientFunction(() => !!window.loaded)
export const numCaptures = ClientFunction(() => window.captures.length)
export const capturesMap = ClientFunction(() => {
const map = {}
window.fullCaptures.forEach((capture) => {
const eventName = capture.event
if (!map[eventName]) {
map[eventName] = 0
}
map[eventName]++
})
return map
})
// test code, doesn't need to be IE11 compatible
// eslint-disable-next-line compat/compat
export const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
// NOTE: This is limited by the real production ingestion lag, which you can see in grafana is usually
// in the low minutes https://grafana.prod-us.insights.dev/d/homepage/homepage
// This means that this test can fail if the ingestion lag is higher than the timeout, so we're pretty
// generous with the timeout here.
export async function retryUntilResults(
operation,
target_results,
{ deadline = undefined, polling_interval_seconds = 30, max_allowed_api_errors = 5, validate } = {}
) {
const start = Date.now()
deadline = deadline ?? start + 10 * 60 * 1000 // default to 10 minutes
let api_errors = 0
let attempts = 0
let last_api_error = null
let last_validation_error = null
let elapsedSeconds = 0
do {
attempts++
let results
try {
results = await operation()
} catch (err) {
api_errors++
last_api_error = err
error('API Error:', err)
}
if (results) {
elapsedSeconds = Math.floor((Date.now() - start) / 1000)
if (validate) {
try {
await validate(results)
log(
`Validated results after ${elapsedSeconds} seconds (attempt ${attempts}, resultCount ${results.length})`
)
return results
} catch (err) {
last_validation_error = err
const message = err instanceof Error ? err.message : String(err)
log(`Validation failed with ${results.length} results (attempt ${attempts}): ${message}`)
}
} else if (results.length >= target_results) {
log(
`Got correct number of results (${target_results}) after ${elapsedSeconds} seconds (attempt ${attempts})`
)
return results
} else {
log(`Expected ${target_results} results, got ${results.length} (attempt ${attempts})`)
}
}
await delay(polling_interval_seconds * 1000)
} while (api_errors < max_allowed_api_errors && Date.now() <= deadline)
if (api_errors >= max_allowed_api_errors && last_api_error) {
throw last_api_error
}
if (last_validation_error) {
const message =
last_validation_error instanceof Error ? last_validation_error.message : String(last_validation_error)
throw new Error(
`Timed out after ${elapsedSeconds} seconds waiting for validated results (attempt ${attempts}). Last validation error: ${message}`
)
}
throw new Error(`Timed out after ${elapsedSeconds} seconds (attempt ${attempts})`)
}
export function dedupeEventsByUuid(events) {
const seen = new Set()
return events.filter((event) => {
if (!event?.uuid) {
return true
}
if (seen.has(event.uuid)) {
return false
}
seen.add(event.uuid)
return true
})
}
export async function queryAPI(testSessionId) {
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString()
const url = `${INSIGHTS_API_HOST}/api/projects/${INSIGHTS_PROJECT_ID}/events?properties=[{"key":"testSessionId","value":["${testSessionId}"],"operator":"exact","type":"event"}]&after=${yesterday}`
const response = await fetch(url, {
headers: HEADERS,
})
const data = await response.text()
if (!response.ok) {
error('Bad Response', response.status, data)
throw new Error('Bad Response')
}
const { results } = JSON.parse(data)
const dedupedResults = dedupeEventsByUuid(results)
if (dedupedResults.length !== results.length) {
log(
`De-duped ${results.length - dedupedResults.length} duplicate ingested events for test session ${testSessionId}`
)
}
return dedupedResults
}
export function log(...args) {
// eslint-disable-next-line no-console
console.log(new Date().toISOString(), ...args)
}
export function error(...args) {
// eslint-disable-next-line no-console
console.error(new Date().toISOString(), ...args)
}
export function santizeTestName(testName) {
return `${testName.replaceAll(/[/ ]/g, '_')}.results.json`
}
export function writeResultsJsonFile(testName, testSessionId, assertFunction) {
fs.writeFileSync(
path.join(__dirname, `${santizeTestName(testName)}.results.json`),
JSON.stringify({ testSessionId, assert: assertFunction.name })
)
}
export function getResultsJsonFiles() {
return fs
.readdirSync(__dirname)
.filter((file) => file.endsWith('.results.json'))
.map((file) => {
const data = fs.readFileSync(path.join(__dirname, file))
return JSON.parse(data.toString())
})
}