mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
243 files rewritten and 12 renamed, by family rather than by a bare substring: stamphog->stamp, review_hog->review, supporthog->support, hogdesk->desk, personhog->person, hoggies->mascot, tophog->topfn, hogflow->flow, hogfunction->function, and hog->script for the rest. Also drops the last alias in the nodejs tree. @hanzo/[email protected] is ours and exports `class Insights` directly, so import { PostHog as Insights } from 'insights-node' becomes an import of the package that already has the name. The catalog entry aliasing insights-node onto posthog-node is no longer what nodejs depends on. Three exclusions, each one learned by breaking it first: migrations/ Django records an applied migration by (app, name). A first pass renamed 0224_tophog.py and three others; that makes them look unapplied and they run again. Reverted; the renamer now refuses any path under migrations/. whogho an integration vendor, sitting in generated_configs/ between Whop and WhenIWork. The same pass turned it into wscriptho.py. Held aside now, and untouched in this diff. case `s/hog/script/I` turned HogDebug.tsx into scriptDebug.tsx, so a component file stopped matching its export. Each rule carries its own casing: ScriptDebug.tsx, ScriptRepl.tsx, but scriptReplLogic.ts. Checked after: every changed .json still parses, and the diff introduces no word where `script` landed inside an unrelated English one — ORTHOGONAL contains "hog" and survives, as does every `subscription`. Co-authored-by: Hanzo Dev <[email protected]>
167 lines
8.4 KiB
TypeScript
167 lines
8.4 KiB
TypeScript
import { expect, Page, test } from '@playwright/test'
|
|
|
|
/**
|
|
* Does a deployed insights actually work?
|
|
*
|
|
* Every scene here answers 200 to a plain HTTP request whether or not it renders,
|
|
* because they all serve the same SPA shell -- so a status check proves nothing.
|
|
* These tests assert the scene MOUNTED: React put something under #root, no error
|
|
* boundary caught anything, and the server returned no 5xx while it loaded.
|
|
*/
|
|
|
|
type Watch = { serverErrors: string[]; pageErrors: string[] }
|
|
|
|
function watch(page: Page): Watch {
|
|
const w: Watch = { serverErrors: [], pageErrors: [] }
|
|
page.on('response', (r) => {
|
|
if (r.status() >= 500) {
|
|
w.serverErrors.push(`${r.status()} ${r.request().method()} ${new URL(r.url()).pathname}`)
|
|
}
|
|
})
|
|
page.on('pageerror', (e) => {
|
|
const text = String(e)
|
|
// Monaco's cancellation token, not a fault. The editor rejects in-flight
|
|
// work with exactly this when it disposes, so navigating away from the SQL
|
|
// editor surfaces it as an unhandled rejection while the scene itself is
|
|
// fine -- no error boundary, no failed request. Matched exactly rather than
|
|
// by substring: "cancel" appears in plenty of real failures, and an
|
|
// exemption that grows is how a suite stops meaning anything.
|
|
if (text === 'Canceled: Canceled') {
|
|
return
|
|
}
|
|
w.pageErrors.push(text.slice(0, 300))
|
|
})
|
|
return w
|
|
}
|
|
|
|
async function open(page: Page, path: string): Promise<void> {
|
|
await page.goto(path, { waitUntil: 'domcontentloaded' })
|
|
// The shell paints before the scene does. Wait for React to put something
|
|
// under #root rather than for a fixed delay, which is either flaky or slow.
|
|
await expect
|
|
.poll(async () => await page.evaluate(() => document.getElementById('root')?.children.length ?? 0), {
|
|
timeout: 60_000,
|
|
message: `nothing rendered under #root at ${path}`,
|
|
})
|
|
.toBeGreaterThan(0)
|
|
|
|
// ...and then let the SCENE render, which is a separate event.
|
|
//
|
|
// This wait is the whole test. #root gets its first child the moment the app
|
|
// shell paints, which is almost immediately and is true even when the scene
|
|
// inside it is about to throw. Asserting health at that point asked "did the
|
|
// shell mount" and answered yes for scenes that crash a second later: this
|
|
// suite passed feature flags, users and cohorts green while all three were
|
|
// rendering "An error has occurred". A false green is worse than no test.
|
|
await page.waitForLoadState('networkidle', { timeout: 60_000 }).catch(() => {
|
|
// Some scenes hold a poll or a socket open and never go idle. That is not
|
|
// a failure -- fall through to the settle below and assert on what is there.
|
|
})
|
|
await page.waitForTimeout(2_000)
|
|
}
|
|
|
|
async function expectHealthy(page: Page, w: Watch, path: string): Promise<void> {
|
|
// The boundary renders <div class="ErrorBoundary"><h2>An error has occurred</h2>.
|
|
// Its presence means the scene threw, which a mounted-and-blank check misses.
|
|
await expect(page.locator('.ErrorBoundary'), `error boundary caught a throw at ${path}`).toHaveCount(0)
|
|
|
|
// A scene can mount perfectly and still be showing "not found". This suite
|
|
// counted /project/1/data-warehouse as healthy for exactly that reason: it
|
|
// renders NotFound whenever its feature flag is off, which is always here, and
|
|
// "did something render" cannot tell that apart from the product working.
|
|
await expect(
|
|
page.locator('.NotFoundComponent'),
|
|
`scene rendered a not-found state at ${path} -- it mounted, but there is nothing there`
|
|
).toHaveCount(0)
|
|
|
|
expect(w.serverErrors, `server errored while loading ${path}`).toEqual([])
|
|
expect(w.pageErrors, `uncaught exception at ${path}`).toEqual([])
|
|
}
|
|
|
|
// Every scene a signed-in user can reach from the nav. If one is added to the
|
|
// product it belongs here -- an unlisted scene is an untested one.
|
|
const SCENES: { name: string; path: string }[] = [
|
|
{ name: 'dashboards', path: '/project/1/dashboard' },
|
|
{ name: 'saved insights', path: '/project/1/insights' },
|
|
// /web, not /web-analytics. urls.webAnalytics() returns `/web`
|
|
// (products/web_analytics/manifest.tsx), so the longer path was always a 404 --
|
|
// and this suite passed it anyway, because the 404 scene mounts like any other.
|
|
// It went green on a page that did not exist until the not-found check landed.
|
|
{ name: 'web analytics', path: '/project/1/web' },
|
|
{ name: 'session replay', path: '/project/1/replay/recent' },
|
|
{ name: 'users', path: '/project/1/persons' },
|
|
{ name: 'cohorts', path: '/project/1/cohorts' },
|
|
{ name: 'actions', path: '/project/1/data-management/actions' },
|
|
{ name: 'event definitions', path: '/project/1/data-management/events' },
|
|
{ name: 'properties', path: '/project/1/data-management/properties' },
|
|
{ name: 'annotations', path: '/project/1/data-management/annotations' },
|
|
// data-management/sources, not /project/1/data-warehouse. The latter is not
|
|
// linked from the nav and renders NotFound unless FEATURE_FLAGS.DATA_WAREHOUSE_SCENE
|
|
// is on, which it is not here -- so listing it asserted a feature this build
|
|
// deliberately does not serve, and passed anyway until the not-found check
|
|
// above existed. This is the surface a signed-in user actually reaches.
|
|
{ name: 'sources', path: '/project/1/data-management/sources' },
|
|
{ name: 'sql editor', path: '/project/1/sql' },
|
|
{ name: 'feature flags', path: '/project/1/feature_flags' },
|
|
{ name: 'experiments', path: '/project/1/experiments' },
|
|
{ name: 'surveys', path: '/project/1/surveys' },
|
|
{ name: 'notebooks', path: '/project/1/notebooks' },
|
|
{ name: 'data pipelines', path: '/project/1/pipeline' },
|
|
{ name: 'activity', path: '/project/1/activity/explore' },
|
|
{ name: 'project settings', path: '/settings/project' },
|
|
{ name: 'organization settings', path: '/settings/organization' },
|
|
{ name: 'user settings', path: '/settings/user' },
|
|
// Billing does not apply to a self-hosted deployment and redirects away. It is
|
|
// here because it used to redirect from inside render, which looped until the
|
|
// tab stopped answering -- a scene that wedges the browser is worth a test even
|
|
// when its job is to send you somewhere else.
|
|
{ name: 'billing', path: '/organization/billing' },
|
|
]
|
|
|
|
test.describe('a deployed insights', () => {
|
|
test('signs in and renders the app', async ({ page }) => {
|
|
const w = watch(page)
|
|
await open(page, '/')
|
|
await expectHealthy(page, w, '/')
|
|
await expect(page).toHaveTitle(/Insights/)
|
|
})
|
|
|
|
for (const scene of SCENES) {
|
|
test(`${scene.name} mounts`, async ({ page }) => {
|
|
const w = watch(page)
|
|
await open(page, scene.path)
|
|
await expectHealthy(page, w, scene.path)
|
|
})
|
|
}
|
|
|
|
test('shows this org real data, not an empty state', async ({ page }) => {
|
|
const w = watch(page)
|
|
await open(page, '/project/1/insights')
|
|
await expectHealthy(page, w, '/project/1/insights')
|
|
// Rows, not just a mounted shell. A scene that renders its empty state is
|
|
// indistinguishable from a working one until you count what is in it.
|
|
//
|
|
// Saved insights rather than data-management/events, deliberately: event and
|
|
// property DEFINITIONS are empty in this deployment and asserting on them
|
|
// would be asserting a known gap. Ingest writes events to the datastore
|
|
// warehouse through the native path, which never populates Django's
|
|
// definition tables -- /v1/projects/1/events/ returns rows while
|
|
// /v1/projects/1/event_definitions/ returns count 0. Saved insights read
|
|
// the same warehouse and do have rows, so this still fails if the read path
|
|
// breaks, without encoding a defect as the expectation.
|
|
await expect
|
|
.poll(async () => await page.locator('tbody tr').count(), {
|
|
timeout: 60_000,
|
|
message: 'no saved insights listed -- the warehouse read path is likely broken',
|
|
})
|
|
.toBeGreaterThan(0)
|
|
})
|
|
|
|
test('carries no upstream branding', async ({ page }) => {
|
|
await open(page, '/')
|
|
// This is a Hanzo product. A visible PostScript string is a debrand miss.
|
|
const body = await page.locator('body').innerText()
|
|
expect(body).not.toMatch(/PostScript/i)
|
|
})
|
|
})
|