adding-activity-logging
Skill727 starsChanged yesterdayMITView on GitHub
Adds or changes activity logging (the audit trail) for a Django model in PostHog. Use when a model's writes must show in the Activity side panel or the advanced activity logs, when adding ModelActivityMixin, an ActivityScope, a model_activity_signal receiver, an activity describer, or field exclusions, when auditing which write paths of a model are logged, or when a change is missing from the activity log. Covers the receiver-module convention, writes the signal cannot see (QuerySet.update, bulk_create), the actor outside requests, and product models on a separate database. Trigger terms - activity log, audit log, audit trail, ModelActivityMixin, log_activity, changes_between, activity describer, who changed this.
We split this skill into the rules it gives an agent
16 rules. About 0 are common, 4 are distinctive, and 12 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
12
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 8
Read docs/internal/activity-logging.md first.
- Very specific: line 26
Write the list down: which paths call save() (the mixin covers them) and which do bulk writes (you log those by hand in step 5).
- Very specific: line 31
Add ModelActivityMixin first in the bases: class Thing(ModelActivityMixin, TeamScopedRootMixin, ...).
- Very specific: line 33
Add the class name to ActivityScope in posthog/models/activity_logging/activity_log.py.
- Very specific: line 34
Add the same string to the ActivityScope enum in frontend/src/types.ts.
- Very specific: line 35
Run hogli build:openapi (needs the dev stack) because the API scope filter enum is generated from the Literal.
- Distinctive: line 37
No migration is needed. The mixin adds no field.
- Very specific: line 46
Keep the module import-light.
- Very specific: line 55
Add model_activity:products.<name>.backend.activity_logging.<handler> to posthog/test/repo_invariants/setup_receivers_baseline.txt.
- Distinctive: line 64
Read the before-values from the writer database before the update.
- Very specific: line 67
Call bulk_log_activity (or log_activity for one row) with user=None when no request user exists, and a Trigger that names the job and its …
- Very specific: line 69
Do not wrap a request path in mute_selected_signals().
- Very specific: line 74
When the model is routed in products/db_routing.yaml:
- Very specific: line 90
Write products/<name>/frontend/activityDescriber.tsx and register it in frontend/src/lib/components/ActivityLog/describers.tsx when the …
- Distinctive: line 96
Add the tests listed under Tests in the doc, and no more.
- Very specific: line 114
Only excluded fields changed (signal_exclusions), or the diff was empty and force_save was not set.
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Adding activity logging
Read docs/internal/activity-logging.md first. It carries the pipeline, the file locations, and the reasons. This skill carries the workflow and the gates.
Reference implementation: products/feature_flags/backend/activity_logging.py with its apps.py.
Separate-database reference: products/stamphog/backend/activity_logging.py.
Step 1 - Find every write path before you touch the model
The mixin hooks save() and delete().
It does not see QuerySet.update(), bulk_create(), bulk_update(), or raw SQL.
A receiver that logs only the API path gives an audit trail that says nobody changed a row a webhook changed.
rg -n "Thing\.objects|Thing\.all_teams" products/<name>/ --type py | rg -v "tests/"
rg -n "\.update\(|bulk_create\(|bulk_update\(" products/<name>/backend --type pyWrite the list down: which paths call save() (the mixin covers them) and which do bulk writes (you log those by hand in step 5).
Include tasks, Temporal activities, webhook handlers, and management commands.
Step 2 - Model and scope
- Add
ModelActivityMixinfirst in the bases:class Thing(ModelActivityMixin, TeamScopedRootMixin, ...). - Set
activity_logging_on_delete = Trueonly when a hard delete must show in the log. - Add the class name to
ActivityScopeinposthog/models/activity_logging/activity_log.py. - Add the same string to the
ActivityScopeenum infrontend/src/types.ts. - Run
hogli build:openapi(needs the dev stack) because the APIscopefilter enum is generated from the Literal.
No migration is needed. The mixin adds no field.
Step 3 - Receiver module
Create products/<name>/backend/activity_logging.py.
One @mutable_receiver(model_activity_signal, sender=Thing) per model.
The receiver calls changes_between and log_activity; nothing else.
Keep the module import-light.
It must not import a viewset, a serializer, or a query runner.
ready() imports it at django.setup() in every process.
For a model without a .team foreign key (ProductTeamModel), read organization_id from Team on the main database inside the receiver.
Step 4 - Wire it
- In
products/<name>/backend/apps.pyready():from products.<name>.backend import activity_logging # noqa: F401, PLC0415, with a one-line comment that says why it must connect in every process. - Add
model_activity:products.<name>.backend.activity_logging.<handler>toposthog/test/repo_invariants/setup_receivers_baseline.txt.
A receiver that is not imported in ready() connects only where something else imports the module.
That is the silent failure this convention exists to prevent.
Step 5 - Bulk writes
For each bulk write from step 1:
- Read the before-values from the writer database before the update.
- Run the update.
- Build one
Changeper field that actually changed, for rows whose value changed. - Call
bulk_log_activity(orlog_activityfor one row) withuser=Nonewhen no request user exists, and aTriggerthat names the job and its id.
Do not wrap a request path in mute_selected_signals().
The flag is process-wide.
Step 6 - Separate-database products only
When the model is routed in products/db_routing.yaml:
- Pass
using=router.db_for_write(Thing)to everylog_activityandbulk_log_activitycall in the product, including the receiver. Without it, the audit row is written before the product row commits and survives its rollback. - The mixin already pins its before-update read to the writer and reads by primary key without a team filter.
Step 7 - Tune the diff
- Fields a job touches on every run go in
signal_exclusions(no row is logged when only they change). - Bookkeeping fields go in
field_exclusions(dropped from the diff). - Secrets go in
field_with_masked_contents(the change is recorded, the values are not). - User-facing names go in
field_name_overrides.
Step 8 - Describer (optional)
defaultDescriber renders "X created / updated / deleted <name>".
Write products/<name>/frontend/activityDescriber.tsx and register it in frontend/src/lib/components/ActivityLog/describers.tsx when the default text does not say what changed in the user's words.
Invoke /writing-user-facing-copy before you write the strings.
Step 9 - Tests
Invoke /writing-tests first.
Add the tests listed under Tests in the doc, and no more.
Verify
hogli test products/<name>/backend/tests/<file>.py
hogli test posthog/test/repo_invariants/test_startup_import_budget.py
ruff check products/<name>/backend/activity_logging.py posthog/models/activity_logging --fixThen check the row in a shell: ActivityLog.objects.filter(scope="Thing").order_by("-created_at").first().detail.
Debugging a missing row
Work down this list; each item is a distinct cause.
- The receiver module is not imported in
ready()(checksetup_receivers_baseline.txtfor the handler name). - The write bypassed
save()(step 1). - Only excluded fields changed (
signal_exclusions), or the diff was empty andforce_savewas not set. ACTIVITY_LOG_TRANSACTION_MANAGEMENTdeferred the write to a commit that never came, or to the wrong connection (step 6).- The row exists but the reader is outside the entitlement's lookback window, or
activity_visibility_restrictionshides it.
Other versions1 project uses a version of this.
Word for word · 1
- PostHog/posthog★ 40k100% same
Edited · 0
None found.
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository124 other files it gives its agents.
- AGENTS.mdAGENTS.md
- common/AGENTS.mdAGENTS.md
- hog/AGENTS.mdAGENTS.md
- proto/AGENTS.mdAGENTS.md
- rust/AGENTS.mdAGENTS.md
- .cursor/rules/django-python.mdcCursor rule
- .cursor/rules/react-typescript.mdcCursor rule
- .cursor/rules/rust.mdcCursor rule
- adding-api-scopesSkill
- adding-inbound-webhooksSkill
- adding-inbox-sourcesSkill
- adding-ingestion-warningsSkill
- adding-mcp-store-serversSkill
- adding-personhog-rpcSkill
- adding-product-alertingSkill
- adding-project-secret-api-key-authSkill
- adding-today-highlightsSkill
- adopting-generated-api-typesSkill
- analyzing-experiment-precompute-canarySkill
- analyzing-experiment-query-performanceSkill
- analyzing-insights-across-teamsSkill
- announcing-behavior-changesSkill
- auditing-llm-gateway-paritySkill
- auditing-warehouse-source-coverageSkill
- authenticating-to-clickhouseSkill
- authoring-ci-workflowsSkill
- building-product-empty-statesSkill
- building-the-toolbarSkill
- clickhouse-migrationsSkill
- debugging-ci-failuresSkill
- debugging-local-replaySkill
- debugging-local-task-agent-runsSkill
- debugging-mcp-analyticsSkill
- debugging-signals-pipelineSkill
- debugging-table-access-deniedSkill
- depot-ciSkill
- depot-container-buildsSkill
- depot-github-runnersSkill
- developing-subscriptionsSkill
- django-migrationsSkill
- django-startup-timeSkill
- documenting-warehouse-sourcesSkill
- editing-agents-mdSkill
- establishing-code-ownershipSkill
- extending-hobby-smoke-testsSkill
- extending-personhog-test-harnessSkill
- finding-llm-gateway-migration-candidatesSkill
- fixing-flaky-testsSkill
- formatting-slack-messagesSkill
- gating-production-deploysSkill
- gating-sensitive-actionsSkill
- generating-clickhouse-query-performance-reportsSkill
- hogliSkill
- implementing-mcp-toolsSkill
- implementing-mcp-ui-appsSkill
- implementing-warehouse-sourcesSkill
- improving-drf-endpointsSkill
- improving-mcp-toolsSkill
- ingestion-pipeline-doctor-nodejsSkill
- instrumenting-first-party-metricsSkill
- The first 60 shown.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

