# Findings — observe: what changed

How the API behaves in this part of a session. Each rule is the entry's own **Teaches**, copied whole.

## 025_event_log

meta.old_value and meta.new_value answer "what was this before", but meta is unfilterable and unsortable: narrow on entity, event_type and attribute_name, sort -id, read meta yourself. **[partial]**

not measured: Whether event_type or meta can be set in the create body is untried: testing it costs another permanent row. Every event read was generated by another user; probe 049 covers the script's own.

The type has 16 fields, all of them server-written. `id` and `created_at` are the only two that order it,
and `meta` is the only one that says what changed.

| field | data type | read | filter |
|---|---|---|---|
| `event_type` | `text` | yes | all 8 text relations |
| `attribute_name` | `text` | yes | all 8 text relations |
| `description` | `text` | yes, a rendered English sentence | all 8 text relations |
| `meta` | `serializable` | yes, decoded | **none**: 400 `cannot be used in a filter` |
| `audit_trail` | `jsonb` | **never returned**, even when named in `fields` | accepted and ignored (`field_types/jsonb`) |
| `entity`, `project`, `user`, `image_source_entity` | `entity` | under `relationships` | `is`, `type_is`, `in`, dotted paths |
| `created_at` | `date_time` | yes | all 15 date_time relations |
| `id` | `number` | yes | `is`, `greater_than`, `less_than`, `between`, `in` |
| `session_uuid` | `uuid` | yes | `is`, `is_not`, `in`, `not_in` |
| `cached_display_name`, `image`, `filmstrip_image`, `image_blur_hash` | `text`, `image` | yes | `is`, `is_not` |

The four production uses, measured:

| use | works | how |
|---|---|---|
| **history**: read a previous value | **yes** | `meta.old_value` and `meta.new_value`, narrowed on `entity` + `event_type` + `attribute_name`, `sort: "-id"` |
| **ledger**: write an entry | **create yes, and it is permanent** | `project` is the only requirement; `event_type` and `meta` are unwritable and `DELETE` is refused |
| **change feed**: consume in id order | **no, not from the head** | ids at the head are 71.3% dense and settle to 100%, so a max-id cursor skips events |
| **lock** | **no** | the read half works; the write half cannot be released, so each acquisition leaks a permanent row |

- **`meta` holds the answer and refuses every query.** `old_value` and `new_value` exist only where
  `meta.type` is `attribute_change`; `new_entity`, `entity_retirement` and `entity_revival` hold
  `entity_id` and `entity_type` and no values at all, and a preference change has no `meta.type` and the
  keys `old`, `new`, `pref`.

- Since `serializable` is unfilterable and unsortable (`field_types/serializable`), select rows by `entity`, `event_type` and `attribute_name`, order by `-id`, and inspect `meta` client-side.

- To restore a previous status: take the newest matching entry, check `meta.new_value` equals the value
  the entity holds now, then write `meta.old_value`. A mismatch means something changed since, and the
  entry is stale.

- **A created entry cannot be deleted, so never write one to a real site.** `POST` with `project` alone
  answers 201, invents `description: "New Event"`, and leaves `event_type`, `attribute_name`, `meta`,
  `user` and `entity` null.

- Every one of those is then refused on `PUT` by a per-field permission rule, and `DELETE` is refused by
  `PermissionRule 297`. Both refusals name a role and a rule number, `API Admin -- PermissionRule 297`, so
  a script user in a different role may be permitted more; check the error before concluding the API
  forbids it everywhere.

- This probe spent its one create on the minimal body and stopped, so **whether `event_type` or `meta` can
  be set in the create body is unmeasured**: testing it costs another permanent row. One row from this
  probe survives in the sandbox project of the probed site. A ledger built here is append-only with no way
  to correct or retract an entry.

- **Ids are reserved ahead of use and committed late.** On the probed site the newest 500 rows spanned 738
  ids with 9 gaps, the largest 33 wide, while every 1001-id window at depth 10000 or more was 100% dense.
  Gaps close, so they are held blocks and not deletions.

- A cursor that stores `max(id)` and asks for `id greater_than <that>` loses whatever later lands in the gaps it passed. Track a low-water mark instead:
  re-scan a window behind the head, or drive the feed from `created_at` and deduplicate on `id`.

- **`entity` goes null when its target is deleted; `meta` remembers.** On the probed site 12889 of 17778
  `Shotgun_Shot_Change` rows have `entity` null, and each names a `meta.entity_id` whose Shot now 404s.
  Filtering on `entity` returns only live targets, so the history of a deleted entity is reachable by
  `event_type` and `created_at` alone.

- **Every event this probe read was generated by somebody else.** On the probed site the script's
  `generate_event_log_entries` was False for the whole window this probe ran in, so none of its own
  writes were logged and the reads above measure other users' events. Probe 049 measures what a
  script's own write puts in the log, and the flag that decides whether it appears at all.

- **Narrowing works on everything but `meta`.** On the probed site the unfiltered log holds 2462044 rows,
  one project 22811, one Shot 2. `event_type` takes `starts_with` and `in`, `created_at` takes `in_last`
  and `between`, and `entity` takes a `{type, id}` hash, `type_is`, and a dotted path such as
  `entity.Shot.code`.

- `attribute_name` alone is site-wide across every entity type, so pair it with `event_type` or `entity`.

- A sort on `meta`, on `audit_trail` or on a name the type does not have is
  accepted and ignored, falling back to ascending `id`, so a client cannot tell an ignored sort from a
  satisfied one.

`corpus/findings/025_event_log.md`

## 043_attention

The six attention calls share no convention with the rest of the API: no paging, no `fields`, `links.self` spelled `/entity/Shot/7668`, and a missing record id on activity_stream is a 500.

- **`activity_stream` is not the event log.** It has its own id space, its own paging keys and its
  own latency. On the probed site the newest update was id `246800` dated two days before the run,
  and three Shots created during the run were absent from their own streams and from the project's
  after 90 seconds of polling.

- Read `EventLogEntry` (`probe 025`) for anything a write has to confirm.

- Page it with `max_id`, not `page[]`. Both bounds are exclusive, `earliest_update_id` is the floor
  reached, and `0` there means the stream ran out. `latest_update_id` is site-wide when no `max_id`
  is given, so it does not describe the record you asked about.

- A record id that is not there answers 500 on `activity_stream`, on all four types tried, where
  `followers`, `following` and `thread_contents` all answer a named 404. Check the record first.

- The follow body's `entity` is the CamelCase schema name and nothing else. `"shots"`, correct in
  every path segment and accepted by `following?entity=`, is a 500 with no clue in it.

- **A partial follow list applies its good half and returns 404.** The 404 names only the missing
  id, so treat the call as non-atomic and re-read `followers`.

- Neither follower list pages or takes `fields`. On the probed site one user's `following` was 896
  rows in a single body, each row id, type and a link, with no name and no follow date.

- `thread_contents` returns Note, Attachment and Reply interleaved by time. The Note and Attachment
  rows name their author under `created_by`, the Reply rows under `user`, and `entity_fields`
  widened the first two and was ignored on the third.

`corpus/findings/043_attention.md`

## 049_script_events

A script's writes reach the event log only while its ApiUser has generate_event_log_entries True. The default is False and nothing errors when off. One create logs one row per field plus one _New.

- **`generate_event_log_entries` on the ApiUser gates the whole feed, and its default is `False`.**
  While it is off the write still answers 201, the log stays empty, and any webhook scoped to that
  change has nothing to fire on. Nothing errors, so a client cannot detect it from the response. Read
  the flag before concluding the API does not log an operation.

- **A script can read and set its own flag.** `PUT /entity/api_users/<own id>` with
  `{"generate_event_log_entries": true}` answers 200. Turning it on is a one-call fix; it also means a
  compromised script key can switch its own audit trail off.

- **One create costs one row per populated field plus one `_New`.** Four keys sent produced five
  `Shotgun_<Type>_Change` rows, because server-set defaults are logged too, and each sets
  `in_create: true`. A consumer counting operations must not count events.

- Events are visible 0.3s to 0.4s after the call. There is no lag to design around at this size.

- `session_uuid` is null on every row this script generated, and set on the rows the web interface
  generated. It does not group a script's calls into a session.

- **A delete logs two rows and orphans all the others in the same instant.** `Shotgun_<Type>_Retirement`
  puts `retirement_date` and `display_name` in `meta`, and every earlier row for that entity drops
  its `entity` link at once, so the history is reachable only through `meta.entity_id`, which is
  unfilterable (probe 025). Capture the id before deleting or lose the trail.

`corpus/findings/049_script_events.md`

## 066_user_feed

A HumanUser's activity_stream is what the person created, not what they follow: 0 of 9 rows touched the 81 followed records. A feed is a fan-out over their tasks' Shots and Assets.

- A HumanUser stream is the rows whose `created_by` is that person, plus the creation of the row
  itself. It is not the Inbox and it is not the follow list: the 77 Tasks and 4 Notes the person
  follows contributed nothing to it, and impersonating the person did not change it.

- A Shot's stream holds its Tasks and Versions as `primary_entity` rows; a Task's stream holds the
  Task alone. A feed of "my day" is one call per distinct Shot or Asset behind the person's Tasks,
  merged on `id` descending, with `max_id` for paging (`endpoints/get_entity_type_id_activity_stream`).

- `read` was false on every row under the script token and under `sudo_as_login`, so the stream does
  not expose the Inbox's read state. Keep the last `id` seen locally and page with `min_id`.

- What a Note or a Reply writes to any stream is `probe 067`.

`corpus/findings/066_user_feed.md`

## 067_notes_in_the_stream

A Reply reaches every linked stream in 33 s as `create_reply`, creates too; a script's Note create and status changes were absent after 430 s. Write as a person.

- **`create_reply` is a fourth `update_type`**, next to `create`, `update` and `delete`
  (`probe 043`). Its `primary_entity` is the Note, named `<subject> - <content>`, and `meta` holds the
  Reply's id and its `content`, so a feed can draw the reply without a second call.

- One update id is written once and fanned out: the same `create_reply` row, id `247337`, was on
  the Note, the Version, the Shot, the Task and the Project. A fan-out over several streams must
  deduplicate on `id`.

- A record's stream holds the creates of its children: the Shot's had the Task and the Version, the
  Task's had the Version. The Version's stream holds no Task. Read the Shot or Asset, not the Task.

- Latency for what does show is under 33 s on the probed site, against the 90 s absence
  `probe 043` measured. Poll at 30 s.

- A Note created by the script user wrote no `create` row on any of six streams, and two status
  changes by the script wrote no `update`, in 430 s. Attribute changes made in the web application
  are on the same streams.

- Autodesk staff on the community forum tie API-made Inbox items to `sudo_as_login` and to the
  script's "Generate Events" flag. A Note created under `scope=sudo_as_login` on the same site was
  on the Shot's stream and on its own within 60 s. Attribute a write to a person (`probe 027`).

- `read_by_current_user` on the Note read `"unread"` for the script, a string, not a boolean.

`corpus/findings/067_notes_in_the_stream.md`
