Worked example: a support page
A backend you host lets signed-in support staff find a customer and see what Anectico observed of them, with unresolved identities, withheld fields and unknown activity shown as they are.
On this page
A page for your support team: type a customer's stable ID or a readable profile value, see which people in Anectico match, open one and see their activity counts, with a link to Anectico's proof page for staff who have their own Anectico access. Support staff sign in to this page, never to Anectico.
Email properties are redacted, so an email lookup works only when the application used that email as its identifier. Prefer stable application IDs; see redaction and customer identity.
The runnable example is scripts/testing/examples/apps/support_page.py in the Anectico repository,
with the shared client anectico_rest.py. Python 3.11 or later, standard library only.
Before you start
| Decision | This example |
|---|---|
| Where it runs | Your support tool's backend. |
| Project | Exactly one, named in every read. |
| Runtime permission | persons:read only. Add persons:profile:read only if the page must show profile properties, and then show only the fields you list in SUPPORT_FIELDS. Session replay and conversation content need their own content permissions and are not part of this example. |
| Builder authority | Your agent's own credential provisions the key; the page never holds it. |
| Secret | ANECTICO_API_KEY from the host's secret store. Staff passwords (SUPPORT_STAFF) are the page's own. |
| Expiry and stop | A fixed expiry; anectico apikey revoke <key-id>. |
| Contract | contract-pin.json; the page refuses to start against another contract. |
What it calls
| Operation | Qualification | What the restriction means for this page |
|---|---|---|
GET /api/v1/persons |
Restricted | The activity summary of each person (impact) can be missing from an answer without an error. The page shows "unavailable" whenever it is missing and never presents a missing summary as no activity, and it does not claim to be a complete health view of a customer. |
GET /api/v1/persons/{person_id}/stats |
Restricted | The counts cover whatever history Anectico still keeps, and the answer does not say how far back that is. The page says so next to the counts. |
The page reads one page of at most ten matches and never follows the cursor: support staff narrow the search instead. A support page is a lookup, not a list of all your customers.
Search
def search(self, text: str) -> dict[str, Any]:
"""One page of people whose identities or properties contain the text."""
answer = self._client.get("/api/v1/persons", {
"project_id": self._config.project_id,
"search": text,
"limit": str(PAGE_SIZE),
}, deadline=10).body
people = []
for person in answer["persons"]:
impact = person.get("impact")
people.append({
"id": person["id"],
"distinct_ids": list(person["distinct_ids"]),
"identified": bool(person["is_identified"]),
# Missing impact means it could not be measured, never zero activity.
"activity": None if impact is None else {
"events": impact["event_count"], "errors": impact["error_count"],
"traces": impact["trace_count"], "last_seen": impact.get("last_seen"),
},
"withheld": list(person.get("content_withheld") or []),
"withheld_reason": person.get("content_withheld_reason") or "",
# Only the fields this page is configured to show leave the backend.
"fields": {k: v for k, v in (person.get("properties") or {}).items() if k in self._config.fields},
})
return {"people": people, "more": bool((answer.get("pagination") or {}).get("has_more"))}
What the page shows, and why:
- Several matches stay several people. When two people match, the page lists both and says they are separate people in Anectico. It never merges them, picks one, or invents a canonical customer.
- Anonymous stays anonymous. A person whose
is_identifiedisfalseis labeled anonymous. - Withheld is shown as withheld. Without
persons:profile:read, profile properties are withheld and the answer says so incontent_withheldandcontent_withheld_reason; the page prints "Withheld by your privacy settings: person_properties (scope)" instead of an empty profile. - Unknown is not zero. A missing activity summary reads "Activity summary: unavailable".
- Only listed fields leave the backend. Even with profile access, the page returns only the property
names in
SUPPORT_FIELDS.
Open one person
def person(self, text: str, person_id: str) -> dict[str, Any]:
"""One person, only if the same search still returns them: the page never opens an arbitrary ID."""
found = next((p for p in self.search(text)["people"] if p["id"] == person_id), None)
if found is None:
raise NotFound("that person is not among the search results", status=404)
path = Client.path("/api/v1/persons/{person_id}/stats", person_id=person_id)
stats = self._client.get(path, {"project_id": self._config.project_id}, deadline=10).body.get("stats")
link = None
if self._config.console_url:
link = proof_link(self._manifest, self._config.console_url, "person", self._config.project_id, id=person_id)
return {"person": found, "stats": stats, "link": link}
The page opens a person only if the same search returns them, so a crafted address cannot read an arbitrary ID. The counts are 64-bit decimal strings and are shown exactly.
The "Open in Anectico" link is built from the proof pages the deployment's contract declares (the
proof_links of the contract manifest), never from a hand-written address. It opens only for someone
signed in to Anectico with access to the project; it grants nothing to anyone else, and the page never
embeds it in a frame.
This example does not show slowest traces, a timeline or a churn prediction: a global "slowest" order and a complete timeline are not part of what these operations promise, and Anectico makes no predictions about your customers.
The page's own API
/?q=<text> searches (3 to 200 characters) and /person?q=<text>&id=<person id> opens a person. Any
other parameter, a repeated parameter, or any other path is refused. Every value is HTML-escaped, the
page carries no script, and request logs record the path only, because a query can contain a
customer's email address.
| Anectico answered | The page shows |
|---|---|
| Busy or unavailable after the retries | 503 "Anectico is busy. Try again shortly." Never "No person matched". |
| A paused limit | 503 "Reading is paused by a limit an owner set." |
401 or 403 |
503 "This page's access to Anectico has ended." |
404 |
"Not found." |
Run it
export ANECTICO_API_URL=https://app.anectico.com ANECTICO_CONSOLE_URL=https://app.anectico.com
export ANECTICO_PROJECT_ID=<project-uuid> ANECTICO_API_KEY="$(your-secret-store read anectico/support-page)"
export SUPPORT_STAFF='dana:a-long-staff-password' SUPPORT_FIELDS=plan
python3 support_page.py
Stop it and clean up
Revoke the page's key. The page does not store what it shows, but browsers and any logs or tickets your staff copy it into are outside Anectico: when you erase a person in Anectico, also delete what your own systems kept about them. See Erase a person.