mirror of
https://github.com/hanzoai/insights-js.git
synced 2026-10-30 18:34:19 +00:00
# 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
254 lines
8.3 KiB
JavaScript
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())
|
|
})
|
|
}
|