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.
// app.osy
use Osysharp.WebAnalytics@0 { egress browser "plausible.io"; }Signature#
[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 itDescription#
Choosing a 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#
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). 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 from the kit's control.
No third-party script runs on your page, so the grant is
egress browser(data), notegress 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#
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 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). Create those goals in Plausible to see them. GA4 receives its own names and parameters (GA4 e-commerce).
A shop: ShopAnalytics()#
Osysharp.Shop depends on this kit. A store puts ShopAnalytics() in its frame, beside ShopVisitSync(), and
reports:
view_itemon a product's page (ShopSetup.ProductPath), for the size a visitor sees first;add_to_cartfor whatever grows in the bag, wherever it was added from;begin_checkouton the checkout (ShopSetup.CheckoutPath,/checkoutunless set);purchaseonce 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#
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#
The control publishes a probe, so a UI test asserts what was reported:
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#
Plausible, in a layout#
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#
- package.osy —
egress browser,egress browser scriptandegress browser consumer;. - What an app page is allowed to load — what an app page may load, and why a grant is needed at all.
- probe — what a control says about itself —
Assert.Probe, which the kit's tests and yours read the control through. - Product feeds — Google Merchant Center and Meta — the shop's product feeds for Google Merchant Center and Meta.