Skip to content
Original file line number Diff line number Diff line change
@@ -1,9 +1,33 @@
from __future__ import annotations

import re
from pathlib import Path

import pytest

from products.posthog_ai.scripts.build_skills import SkillRenderer
from products.posthog_ai.scripts.schema_columns import schema_columns

REFERENCES = Path(__file__).resolve().parents[3] / "skills" / "querying-posthog-data" / "references"

# A four-column row (`col` | type | nullable | description) is a schema table. The two-column
# `field` | description tables in these docs describe nested JSON, not columns.
HAND_WRITTEN_COLUMN_ROW = re.compile(r"^`[^`]+`\s*\|.*\|.*\|")


def test_model_references_render_without_hand_written_columns() -> None:
renderer = SkillRenderer()
paths = sorted(REFERENCES.glob("models-*.md.j2"))
assert paths, f"no model reference templates found in {REFERENCES}"

for path in paths:
renderer.render(path) # raises if a referenced table no longer exists in the catalog
offenders = [line for line in path.read_text().splitlines() if HAND_WRITTEN_COLUMN_ROW.match(line)]
assert not offenders, (
f"{path.name} hand-writes column rows instead of calling schema_columns(): {offenders[:3]}. "
"Columns drift out of sync with HogQL when copied from the Django models."
)


def test_unknown_table_raises() -> None:
with pytest.raises(ValueError, match="system.not_a_table"):
Expand All @@ -20,3 +44,43 @@ def test_renders_only_columns_hogql_exposes() -> None:
# Django-model fields HogQL does not expose, which the docs used to advertise.
for absent in ("is_sample", "derived_name", "filters_hash", "refreshing"):
assert absent not in rendered


def test_renders_session_recording_nullability() -> None:
rendered = schema_columns("system.session_recordings")

for column in (
"distinct_id",
"duration",
"active_seconds",
"inactive_seconds",
"start_time",
"end_time",
"click_count",
"keypress_count",
"mouse_activity_count",
"console_log_count",
"console_warn_count",
"console_error_count",
"start_url",
"deleted",
"created_at",
"retention_period_days",
"storage_version",
):
assert f"`{column}` | " in rendered
assert (
next(line for line in rendered.splitlines() if line.startswith(f"`{column}` | ")).split(" | ")[2]
== "NOT NULL"
)


def test_renders_usage_metric_contract() -> None:
rendered = schema_columns("system.usage_metrics")

assert (
"`group_type_index` | Integer | NOT NULL | Legacy; the query runner ignores it and evaluates every metric regardless. Don't filter on it."
in rendered
)
assert "`format` | String | NOT NULL | Display format: 'numeric' or 'currency'." in rendered
assert "`math` | String | NOT NULL | Aggregation: 'count' or 'sum'; 'sum' aggregates math_property." in rendered
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,7 @@ Actions are named combinations of events and conditions used for filtering and a

### Columns

Column | Type | Nullable | Description
`id` | integer | NOT NULL | Primary key (auto-generated)
`name` | varchar(400) | NULL | Action name
`description` | text | NOT NULL | Action description
`deleted` | boolean | NOT NULL | Soft delete flag
`created_at` | timestamp with tz | NOT NULL | Creation timestamp
`updated_at` | timestamp with tz | NOT NULL | Last update timestamp
`steps_json` | jsonb | NULL | Action step definitions
`post_to_slack` | boolean | NOT NULL | Whether to post matches to Slack
`slack_message_format` | varchar(1200) | NOT NULL | Slack message template
`is_calculating` | boolean | NOT NULL | Whether calculation is in progress
`last_calculated_at` | timestamp with tz | NOT NULL | Last calculation timestamp
`bytecode` | jsonb | NULL | Compiled action bytecode
`bytecode_error` | text | NULL | Compilation error message
`pinned_at` | timestamp with tz | NULL | When action was pinned
`summary` | text | NULL | AI-generated summary
`created_by_id` | integer | NULL | Creator user ID
{{ schema_columns('system.actions') }}

### Steps JSON Structure

Expand Down Expand Up @@ -64,7 +48,6 @@ Field | Description

- Actions can combine multiple event conditions (steps)
- Steps are OR'd together - matching any step triggers the action
- `bytecode` is compiled from steps for efficient evaluation
- Actions can be used in insights, cohorts, and feature flag targeting

---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,7 @@ Activity logs track user and system actions across PostHog entities, providing a

### Columns

Column | Type | Nullable | Description
`id` | uuid | NOT NULL | Primary key (auto-generated UUID)
`team_id` | integer | NULL | Team the activity belongs to
`activity` | varchar(79) | NOT NULL | Action performed (e.g., `created`, `updated`, `deleted`)
`item_id` | varchar(72) | NULL | ID of the entity being logged (may be numeric ID, short ID, or UUID)
`scope` | varchar(79) | NOT NULL | Entity type being logged (e.g., `FeatureFlag`, `Insight`)
`detail` | jsonb | NULL | Structured details about the change
`created_at` | timestamp with tz | NOT NULL | When the activity occurred
{{ schema_columns('system.activity_logs') }}

### Detail JSON Structure

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# AI observability evaluations

## Evaluation directory (`system.evaluation_directories`)

Evaluation directories organize online evaluations. Directories are flat. An evaluation with no directory is at the top level.

### Columns

{{ schema_columns('system.evaluation_directories') }}

## Evaluation (`system.evaluations`)

Online evaluations score AI generations or traces. Evaluation results are stored as `$ai_evaluation` events, not on this configuration table.

### Columns

{{ schema_columns('system.evaluations') }}

### Important notes

- Filter on `deleted = false` to match the default online evals list.
- Use `directory_id IS NULL` for evaluations at the top level.
- Deleting a directory preserves its evaluations and sets their `directory_id` to NULL.

## Common query patterns

**List directories with active evaluation counts:**

```sql
SELECT
d.id,
d.name,
count(e.id) AS evaluation_count
FROM system.evaluation_directories AS d
LEFT JOIN system.evaluations AS e
ON e.directory_id = d.id
AND e.deleted = false
GROUP BY d.id, d.name
ORDER BY d.name ASC
```

**List active evaluations at the top level:**

```sql
SELECT id, name, evaluation_type, status, updated_at
FROM system.evaluations
WHERE deleted = false
AND directory_id IS NULL
ORDER BY updated_at DESC
LIMIT 100
```
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,7 @@ Each active trace can have at most one active review at a time.

### Columns

Column | Type | Nullable | Description
`id` | uuid | NOT NULL | Primary key
`team_id` | integer | NOT NULL | Owning team
`trace_id` | varchar(255) | NOT NULL | Reviewed LLM trace ID
`created_by_id` | integer | NULL | User ID that originally created the review
`reviewed_by_id` | integer | NULL | User ID that last saved the review
`comment` | text | NULL | Optional comment attached to the review
`created_at` | timestamp with tz | NOT NULL | Creation timestamp
`updated_at` | timestamp with tz | NULL | Last save timestamp
`deleted` | integer | NULL | Soft-delete flag (`0` = active, `1` = deleted)
`deleted_at` | timestamp with tz | NULL | When the review was soft-deleted
{{ schema_columns('system.trace_reviews') }}

### Key relationships

Expand All @@ -33,20 +23,7 @@ Each row captures one scorer definition and exactly one value type.

### Columns

Column | Type | Nullable | Description
`id` | uuid | NOT NULL | Primary key
`team_id` | integer | NOT NULL | Owning team
`review_id` | uuid | NOT NULL | FK to `system.trace_reviews.id`
`definition_id` | uuid | NOT NULL | Stable scorer definition ID
`definition_version` | uuid | NOT NULL | Immutable scorer version ID used when saving the score
`definition_version_number` | integer | NOT NULL | Immutable scorer version number used when saving the score
`definition_config` | jsonb | NOT NULL | Snapshot of the scorer configuration used for validation
`categorical_values` | array(varchar) | NULL | Selected categorical option keys
`numeric_value` | decimal(12,6) | NULL | Saved numeric score
`boolean_value` | boolean | NULL | Saved boolean score
`created_by_id` | integer | NULL | User ID that created the score row
`created_at` | timestamp with tz | NOT NULL | Creation timestamp
`updated_at` | timestamp with tz | NULL | Last update timestamp
{{ schema_columns('system.trace_review_scores') }}

### Important notes

Expand All @@ -61,15 +38,7 @@ Review queues are named buckets used to route traces that still need review.

### Columns

Column | Type | Nullable | Description
`id` | uuid | NOT NULL | Primary key
`team_id` | integer | NOT NULL | Owning team
`name` | varchar(255) | NOT NULL | Display name for the queue
`created_by_id` | integer | NULL | User ID that created the queue
`created_at` | timestamp with tz | NOT NULL | Creation timestamp
`updated_at` | timestamp with tz | NULL | Last update timestamp
`deleted` | integer | NULL | Soft-delete flag (`0` = active, `1` = deleted)
`deleted_at` | timestamp with tz | NULL | When the queue was soft-deleted
{{ schema_columns('system.review_queues') }}

### Key relationships

Expand All @@ -84,16 +53,7 @@ An active trace can only be pending in one queue at a time.

### Columns

Column | Type | Nullable | Description
`id` | uuid | NOT NULL | Primary key
`team_id` | integer | NOT NULL | Owning team
`queue_id` | uuid | NOT NULL | FK to `system.review_queues.id`
`trace_id` | varchar(255) | NOT NULL | Pending LLM trace ID
`created_by_id` | integer | NULL | User ID that queued the trace
`created_at` | timestamp with tz | NOT NULL | Creation timestamp
`updated_at` | timestamp with tz | NULL | Last update timestamp
`deleted` | integer | NULL | Soft-delete flag (`0` = active, `1` = deleted)
`deleted_at` | timestamp with tz | NULL | When the queue item was soft-deleted
{{ schema_columns('system.review_queue_items') }}

### Important notes

Expand All @@ -109,17 +69,7 @@ Each scorer has a stable identity but config is versioned and immutable — bump

### Columns

Column | Type | Nullable | Description
`id` | uuid | NOT NULL | Stable scorer ID
`team_id` | integer | NOT NULL | Owning team
`name` | varchar(255) | NOT NULL | Display name
`description` | text | NOT NULL | Optional description (defaults to empty string)
`kind` | varchar(32) | NOT NULL | One of `categorical`, `numeric`, `boolean`. Immutable after creation
`archived` | boolean | NOT NULL | Whether the scorer is archived (hidden from default lists)
`current_version_id` | uuid | NULL | FK to the latest `score_definition_versions` row (config + version number)
`created_by_id` | integer | NULL | User ID that created the scorer
`created_at` | timestamp with tz | NOT NULL | Creation timestamp
`updated_at` | timestamp with tz | NULL | Last metadata update timestamp
{{ schema_columns('system.score_definitions') }}

### Key relationships

Expand Down

This file was deleted.

Loading
Loading