Files
zeekayandHanzo Dev 7752771290 no hogs: the frontend, common, services and playwright
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]>
2026-08-09 09:04:35 -07:00

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)
})
})