Skip to content
Console
Browse documentation
Guide

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.

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_identified is false is labeled anonymous.
  • Withheld is shown as withheld. Without persons:profile:read, profile properties are withheld and the answer says so in content_withheld and content_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.