# Clicks and views

Report what shoppers click and see after a search, so Insights shows the searches that find what nobody opens.

A search with results is not yet a good search. [Insights](/docs/insights) counts what shoppers click and see per query, so a search that finds products nobody opens shows as one. The React components report by themselves. A shop that draws its own results reports through the client, with the hit the answer named:

```ts twoslash title="app/results.ts"
import { createSearch } from "@orbsearch/client";

const search = createSearch({ endpoint: "http://127.0.0.1:7800" });
const answer = await search.query("", { on: "category_page", category: "wohnen/sofas" });
const sofa = { query_id: answer.query_id, entity: "product:SOFA-ASKA-2", position: 1 };

export function reportTile(tile: Element) {
    tile.addEventListener("click", () => search.report.click(sofa)); // sent at once
    return search.report.watch(tile, sofa); // a view, once half of it was visible for a second
}

search.report.view({ query_id: answer.query_id, placement: "home/sofa-sale" }); // seen now, such as a banner
search.report.flush(); // the waiting views, before the shop replaces the page itself
```

Each report is an [event](/docs/reference/glossary#event) naming the [query ID](/docs/reference/glossary#query-id) of its answer, here the sofa page's, and the hit by its [entity](/docs/reference/glossary#entity) or the banner by its [rule](/docs/reference/glossary#rule). `watch` returns the call that stops watching.

## What the components report

A tile, a banner, a suggestion and a link to a hit beside the products each report two things:

- **A click** on a link in them, a middle click included. It goes at once by `navigator.sendBeacon`, so a navigation never waits for it.
- **A view** once half of the item was visible for a second, once per answer and item. Views wait in memory and go together when the page is hidden, or once 100 have gathered.

A tile "Load more" brought names the answer of the first page, with its position counted across the pages, and a sponsored one names its campaign. The query ID never goes into a link, and nothing is written to the shopper's device.

## Lists of your own in React

A React list of your own names its answer with `<Answer>`, and each item reports through `useReport`, whose `ref` and click handlers go on the item's element:

```tsx twoslash title="app/sofa-link.tsx"
import { Answer, useReport } from "@orbsearch/react";

export function SofaLinks({ queryId }: { queryId: string }) {
    return (
        <Answer queryId={queryId}>
            <SofaLink entity="product:SOFA-ASKA-2" position={1} />
        </Answer>
    );
}

function SofaLink({ entity, position }: { entity: string; position: number }) {
    const report = useReport({ entity, position });
    return (
        <li {...report}>
            <a href="/p/aska-2-sitzer-sofa">Aska 2-Sitzer-Sofa aus Samt</a>
        </li>
    );
}
```

Both need a `StorefrontProvider` above them, whose client sends the reports.

## Send nothing

Make the client with `report: false`, and every report call does nothing, the components' included. The demo storefront does so for a browser that sends [Global Privacy Control](https://globalprivacycontrol.org/):

```ts twoslash title="app/search.ts"
import { createSearch } from "@orbsearch/client";

const privacyAsked =
    typeof navigator !== "undefined" && "globalPrivacyControl" in navigator && navigator.globalPrivacyControl === true;

export const search = createSearch({ endpoint: "http://127.0.0.1:7800", report: !privacyAsked });
```

A shop that asks for consent makes its client with `report: false` until the shopper agrees. If they take it back, a client made again for the same `pageView` with `report: false` also drops the views still waiting. On a server, where no beacon can go, the calls do nothing either.

## The Events API underneath

The client posts each batch to `POST /v1/events` on the Query API, with no key and no cookie. Here is a click on the first hit of a search for "graues Sofa" (grey sofa):

```sh title="Terminal"
query_id=$(curl -s 'http://127.0.0.1:7800/v1/search?query=graues%20Sofa' | jq -r .query_id)
curl -s http://127.0.0.1:7800/v1/events -H 'Content-Type: text/plain' \
  -d '{"events": [{"type": "click", "query_id": "'"$query_id"'", "entity": "product:SOFA-ASKA-2", "position": 1}]}'
```

```json title="Answer: 202 · 2.8 ms"
{"accepted":1,"late":0}
```

The answer is `202`: `accepted`, and `late` for events whose answer is older than 48 hours. The body is read as JSON whatever its content type, since a beacon posts `text/plain`. A batch holds 1 to 100 events in at most 64 kB, and one malformed event refuses it whole with `400`.

**Reference:** [`POST /v1/events`](/docs/reference/events-api#events) · [`events_per_call`](/docs/reference/limits#events_per_call) · [`event_hours`](/docs/reference/limits#event_hours)

## Next

- [Insights](/docs/insights): what the merchant reads from clicks and views.
- [React components](/docs/react): the parts that report by themselves.
- [The client](/docs/client): one client per page load, and its page view.
