# The analytics kit — Plausible or Google Analytics 4, with consent

> `Osysharp.WebAnalytics` reports an app's page views and e-commerce events to Plausible or Google Analytics 4. The app picks the provider in its config and grants that provider's hosts on its `use` line; `Analytics()` in the layout does the rest. GA4 loads nothing until the visitor accepts the consent bar; Plausible needs no consent.

<!-- id: ui-analytics-kit · area: ui · stability: preview · html: https://osysharp.com/reference/ui/analytics-kit/ -->

## Summary        {#summary}
`Osysharp.WebAnalytics` counts visits and sales with **Plausible** or **Google Analytics 4**. You choose the provider in
the app's config, grant the hosts it talks to on the kit's `use` line, and put `Analytics()` in your layout once.
Every page view is then reported, along with any e-commerce events the layout hands it.

The events are GA4's recommended e-commerce names: `view_item`, `add_to_cart`, `begin_checkout` and `purchase`. A shop
built on `Osysharp.Shop` does not hand it any: it writes `ShopAnalytics()` in its frame instead, which derives all
four from the shop's own rows.

```osy syntax
// app.osy
use Osysharp.WebAnalytics@0 { egress browser "plausible.io"; }
```

## Signature      {#signature}
```osy syntax
[AppSetting("Analytics")] class AnalyticsSetup
    // Provider · Domain · Host (Plausible) · MeasurementId (GA4)
enum AnalyticsProvider            // None · Plausible · GoogleAnalytics
class AnalyticsEvent              // Name · Key · Currency · Value · TransactionId · Tax · Shipping · Items
class AnalyticsItem               // Id · Name · Brand · Category · Variant · Price · Quantity

component Analytics(AnalyticsEvent[] events, AnalyticsItem[] cart, string currency, Action<string> onSent)
component AnalyticsConsentBar(Action accept, Action decline)
control AnalyticsTag              // the browser half — probe: sent · lastPageView · lastViewItem · lastAddToCart ·
                                  //   lastBeginCheckout · lastPurchase · purchases · loaded · consent · dryRun
AnalyticsConfig AnalyticsConfiguration()   // the setup, as the page needs it
```

## Description    {#description}

### Choosing a provider   {#provider}
`app.Analytics` is read fresh on every page, so changing it and recompiling is all a switch takes.

| Provider | Set | Grant on `use Osysharp.WebAnalytics@0 { … }` |
|---|---|---|
| Plausible | `Domain` — the site as registered in Plausible | `egress browser "plausible.io";` |
| Plausible, self-hosted | `Domain`, and `Host = "https://stats.example.com"` | `egress browser "stats.example.com";` |
| GA4 | `MeasurementId = "G-XXXXXXXXXX"` | `egress browser script "*.googletagmanager.com";` `egress browser "*.google-analytics.com";` `egress browser "*.analytics.google.com";` |

With `Provider = None`, or no domain or id set, nothing is loaded and nothing is sent.

### No snippet is pasted into the page   {#no-snippet}
An app page carries a Content-Security-Policy that admits code and connections from the app's own origin only (see
[What an app page is allowed to load](https://osysharp.com/reference/ui/content-security-policy/)). The kit does not get around it. Its browser code is its own control, pinned by
content hash like any control, and the hosts it reaches are exactly the ones the app grants. The kit's manifest says
`egress browser consumer;`, because which host is the app's choice: Plausible may be self-hosted. The grants appear
with every other egress grant in `osy egress list` and in the Admin app's Config tab. A host nobody granted is refused
by the browser.

- **Plausible** is spoken to through its [Events API](https://plausible.io/docs/events-api) from the kit's control.
  No third-party script runs on your page, so the grant is `egress browser` (data), not `egress browser script`.
- **GA4** has no browser client other than Google's tag, so the tag is loaded, from the host you granted as
  `egress browser script`. That grant is the largest a package can ask for, which is one more reason it waits for
  consent.

### Consent   {#consent}
Plausible sets no cookie and keeps no personal data, so the kit asks nothing and reports from the first page.

GA4 sets cookies, and in the EU it may not track before the visitor agrees. With GA4, `Analytics()` shows
`AnalyticsConsentBar` along the foot of the page: a *Decline* and an *Accept* of equal weight. **Until the visitor
accepts, nothing of Google's is loaded**: no script, no request, no cookie. Events from before consent are dropped,
not queued, because an event from before consent is what consent exists to prevent. Accepting counts the page it was
given on. The answer is kept in the visitor's browser, so the bar does not ask again. Consent mode is set explicitly:
analytics storage as answered, advertising storage always denied, because the visitor was asked about counting
visits, not about ads.

### Events   {#events}
`events` are the events true of the page being viewed, such as "this page shows product X". Each is reported once per
page view, identified by its `Key`. A `purchase` is reported once in a browser, however often the page is reloaded:
its key is remembered before it is sent. `cart` is the visitor's bag. Whatever grows in it is an `add_to_cart`, and a
bag that loads empty first is not read as "everything was just added". `onSent` hears each event's key once it has
gone, which is how the shop stops offering a sale it has reported.

Plausible receives the events as goals named *View Item*, *Add to Cart*, *Begin Checkout* and *Purchase*, and a
purchase carries its revenue ([Plausible's revenue tracking](https://plausible.io/docs/ecommerce-revenue-tracking)).
Create those goals in Plausible to see them. GA4 receives its own names and parameters
([GA4 e-commerce](https://developers.google.com/analytics/devguides/collection/ga4/ecommerce)).

### A shop: `ShopAnalytics()`   {#shop}
`Osysharp.Shop` depends on this kit. A store puts `ShopAnalytics()` in its frame, beside `ShopVisitSync()`, and
reports:

- `view_item` on a product's page (`ShopSetup.ProductPath`), for the size a visitor sees first;
- `add_to_cart` for whatever grows in the bag, wherever it was added from;
- `begin_checkout` on the checkout (`ShopSetup.CheckoutPath`, `/checkout` unless set);
- `purchase` once per order. It is reported from the **order**, not from a thank-you page, which can be reloaded or
  never reached. Placing an order with analytics on leaves a sale for the visit that placed it. The frame reports it
  once the order is paid, or at once for a way to pay with no payment page (a bank transfer, the counter), and then it
  is gone.

### A domain that cannot be a site   {#dry-run}
A Plausible `Domain` under a reserved name (`.example`, `.test`, `.invalid`, `.localhost`) cannot be a Plausible
site. The control records its events and sends none, and its probe says `dryRun`. That is what a demo or a test
wants: the events are real and assertable, and nothing leaves the machine.

### Testing it   {#testing}
The control publishes a probe, so a UI test asserts what was reported:

```osy syntax
Ui.Visit("/p/kiambu-aa");
Assert.Probe("AnalyticsTag", "lastViewItem", "Kiambu AA (250 g)");
Ui.Click("Add to bag");
Assert.Probe("AnalyticsTag", "lastAddToCart", "1 × Kiambu AA (250 g)");
```

## Examples       {#examples}

### Plausible, in a layout   {#example-plausible}
```osy title="every page view counted by Plausible" test app=ui-analytics-kit
app AnalyticsExample {
  use Osysharp.Ui;
  use Osysharp.WebAnalytics@0 { egress browser "plausible.io"; }
  model "model/**/*.osy";
}

using Osysharp.WebAnalytics;

app.Analytics = new AnalyticsSetup { Provider = AnalyticsProvider.Plausible, Domain = "kettles.example.com" };

[Layout]
[AllowAnonymous]
component Site() {
  action Heard(string key) { }
  render {
    Stack(gap: 4) {
      Analytics(events: [], cart: [], currency: "SEK", onSent: Heard);
      Outlet();
    }
  }
}

[Route("/")]
[Layout(Site)]
[AllowAnonymous]
component Home() {
  render { Text("Kettles, and nothing else."); }
}
```

## See also       {#see-also}
- [package.osy](https://osysharp.com/reference/project/package/) — `egress browser`, `egress browser script` and `egress browser consumer;`.
- [What an app page is allowed to load](https://osysharp.com/reference/ui/content-security-policy/) — what an app page may load, and why a grant is needed at all.
- [probe — what a control says about itself](https://osysharp.com/reference/ui/control-probe/) — `Assert.Probe`, which the kit's tests and yours read the control through.
- [Product feeds — Google Merchant Center and Meta](https://osysharp.com/reference/api/product-feeds/) — the shop's product feeds for Google Merchant Center and Meta.
