Pre-launch API. The concepts described here are settled, but the API shape is not: request and response fields, parameters and defaults can still change. Build against it, and talk to your Luigi's Box contact before you put an integration into production.
Luma sends analytics events to Luigi’s Box as your integration’s
_global/_analytics.ts describes them. Every event sent is one collect.<event> line in
that file, and nothing else is sent: the file read top to bottom is what your site collects.
Without the file there is no analytics — no code is loaded and nothing is sent.
import { collect, dl, fromGa4, fromLuma, lumaCarts, lumaClicks, lumaPages, ref } from '@/analytics';
ref.product.by('sku'); // product references by the catalog's `sku` field: product/sku:<value>
collect.pageView.on(lumaPages()).read(fromLuma.pageView);collect.click.on(lumaClicks()).read(fromLuma.click);collect.addToCart.on(lumaCarts()).read(fromLuma.addToCart);collect.addToCart.on(dl('add_to_cart')).read(fromGa4.addToCart);collect.purchase.on(dl('purchase')).read(fromGa4.purchase);__integration.ts hands the file to Luma as a lazy glob:
analytics: import.meta.glob('./*/_analytics.ts', { import: 'default' }),analytics: false in _global/_config.ts turns analytics off whatever the file says.
A line per event
Section titled “A line per event”Each line names the event, when it happens and how it is read:
collect.addToCart // the event sent: an add-to-cart .on(dl('add_to_cart')) // when: each dataLayer push with event 'add_to_cart' .read(fromGa4.addToCart) // how: reads the standard GA4 ecommerce event and remaps it to a Luigi's Box analytics event.read() takes a reader or a function of the trigger’s context that returns what
the event sends; null sends nothing. Before .read():
.when(f) | Only when f(context) is true |
.throttle(ms) | At most once per ms; the rest are dropped |
.once() | At most once per page |
A file can have any number of lines for the same event, from different triggers.
Events
Section titled “Events”| Event | Sent as | What .read() returns |
|---|---|---|
pageView | A page view | The page’s reference, or null for a page without one. One pageView line per file — see Page views. |
click | interaction, click | A reference, or { reference } |
addToCart | interaction, add_to_cart | { reference, count?, price? }, or a list of them. price is the unit price. |
purchase | transaction | { id?, currency?, items: [{ reference, title, count, total_price, was_discounted? }] }. Sent once per id, so a reloaded thank-you page does not count twice. |
search, autocomplete, recommender(name) | The results your own service showed | { query?, items, filters? } — see Your own search and suggestions |
A click or add-to-cart without a valid reference is not sent. currency defaults to
_global.currency.
Page views
Section titled “Page views”On lumaPages(), one page view per page, sent at once.
On any other trigger, each page still sends one page view, and waits for its reference from
that trigger — the product of a product page, read from GA4’s view_item. After
.timeout(ms) (1000 ms by default) it is sent without one.
collect.pageView.on(dl('view_item')).timeout(1500).read(fromGa4.pageView);A page is a URL without its tracking parameters (gclid, utm_*) and its hash. Filtering,
sorting or “show more” on a Luma listing is not a new page.
Triggers
Section titled “Triggers”| Trigger | When | Context |
|---|---|---|
lumaPages() | Each page: the first, the next page of a single-page app, a page restored from the back/forward cache, a prerendered page once it is shown | { url, referrer } |
lumaClicks() | A click on a product in a Luma widget: any link to it, middle and modifier clicks included, and a pick in the autocomplete | { reference, result, index, list } |
lumaCarts() | A click on a [data-lbx-cart] button in a Luma product tile — see Add to cart from a tile | { reference, result, index, list, count, price? } |
dl(event) | A dataLayer push with that event name, in object or gtag() form, including pushes made before Luma loaded | { event, eventType, data, ecommerce } |
domClick(selector) | A click or middle click on an element of your page matching selector | { element, event } |
urlChange() | A navigation in a single-page app | { url, previousUrl } |
mutationOf(selector, { root? }) | Your page renders that element, or renders into it; read once the browser is idle | { element } |
xhrMatching(({ url, method }) => …) | A response to one of your page’s own requests, XMLHttpRequest or fetch, that the test matches | { url, method, response } |
manual() | fire(data) is called: const { trigger, fire } = manual() | { data } |
dl() reads window.dataLayer. _global.dataLayer in _config.ts names another layer.
Readers
Section titled “Readers”| Reader | Reads |
|---|---|
fromLuma.pageView, .click, .addToCart | What Luma’s own triggers saw — lumaPages(), lumaClicks(), lumaCarts(). Products are referenced by their @id. |
fromGa4.addToCart, .purchase, .click, .pageView, .search, .autocomplete, .recommender | GA4 ecommerce: add_to_cart, purchase, select_item, view_item, view_item_list |
The GA4 readers take the push’s ecommerce object (or gtag()’s parameters), or the push
itself when its items are on it. An item is referenced by ref.product(item_id); an item
without item_id is skipped. A purchase gets each line’s total — price × quantity — and
was_discounted for a line with a discount. A list reader takes the query from
search_term.
Each reader belongs to one event: collect.purchase.on(…).read(fromGa4.addToCart) is a type
error, and throws when the file runs.
References
Section titled “References”Events name catalog objects by reference,
<type>/<field>:<value>. ref builds them:
ref.product('sku-1001'); // product/@id:product/sku-1001ref.product('ean', '5901234123457'); // product/ean:5901234123457ref.product.by('sku'); // ref.product(value) uses `sku` from here onref.product('ABC-123'); // product/sku:ABC-123ref.variant, ref.category and ref.brand work the same way. The default field is @id.
Products in Luma’s own widgets are always referenced by their @id, whatever
ref.product.by() says.
References from GA4 events
Section titled “References from GA4 events”The fromGa4 readers reference each item as ref.product(item_id): the item_id of the
GA4 item, as the value of the field set by ref.product.by(). That field must be a catalog
attribute holding the same values your dataLayer sends as item_id. If item_id is your
SKU, name the attribute your catalog stores the SKU in:
// dataLayer: { event: 'add_to_cart', ecommerce: { items: [{ item_id: 'ABC-123', … }] } }ref.product.by('sku'); // sent as product/sku:ABC-123A reference that names no catalog object is recorded and then
joins to nothing. With the
default @id, item_id must be the product’s @id value.
A different reference for one event
Section titled “A different reference for one event”ref.product.by() applies to every event. When one event’s item_id holds a different
identifier — the product code on view_item and add_to_cart, the variant’s EAN on
purchase — read that event with your own function and build its references there:
ref.product.by('sku'); // view_item, add_to_cart: item_id is the SKU
// purchase: item_id is the variant's EANcollect.purchase.on(dl('purchase')).read(({ ecommerce }) => ({ id: ecommerce?.transaction_id, currency: ecommerce?.currency, items: (ecommerce?.items ?? []).map((i) => ({ reference: ref.variant('ean', i.item_id), title: i.item_name, count: i.quantity ?? 1, total_price: (i.price ?? 0) * (i.quantity ?? 1), })),}));ref.<type>(field, value) names the field for that one reference and leaves the default
unchanged.
Signed-in shoppers
Section titled “Signed-in shoppers”import { user } from '@/analytics';
user.identify(() => window.shop?.customerId ?? null);The function is read on every event and its value sent as
user_id. null or '' sends none.
Add to cart from a tile
Section titled “Add to cart from a tile”A button in a Luma product tile adds to your cart when it carries data-lbx-cart, its value
the quantity (1 when empty):
html`<button type="button" data-lbx-cart="1">Add to cart</button>`A click on it calls _global.addToCart(result, { count, el, event }), which you set in
_global/_init.ts or _global/_widget.ts to put the product in your cart. It works with or
without analytics. With lumaCarts() in _analytics.ts, the click is also an add-to-cart
event.
When your cart then pushes its own add_to_cart to the dataLayer, within 10 seconds of the
click, that push is not counted a second time.
Your own search and suggestions
Section titled “Your own search and suggestions”Where your site still renders its own search results, suggestions or recommendations, send
what they showed with collect.search, collect.autocomplete and
collect.recommender(name), so clicks on them are attributed.
Each item is { title, type, reference, position }, with price when you have it. A
fromGa4 reader on view_item_list builds them, or read your page:
import { collect, dl, domClick, fromGa4, mutationOf, ref, xhrMatching } from '@/analytics';
// Your search results, rendered into #resultscollect.search.on(mutationOf('#results')).once().read(({ element }) => ({ query: { string: new URLSearchParams(location.search).get('q') ?? '' }, items: [...element.querySelectorAll('[data-sku]')].map((tile, n) => ({ title: tile.textContent ?? '', type: 'product', reference: ref.product(tile.getAttribute('data-sku')!), position: n + 1, })),}));
// Clicks on those resultscollect.click .on(domClick('#results [data-sku] a')) .read(({ element }) => ref.product(element.closest('[data-sku]')!.getAttribute('data-sku')!));
// Your suggestions, from your own APIcollect.autocomplete.on(xhrMatching(({ url }) => url.includes('/suggest'))).read(({ response }) => ({ items: [], // read from response}));
// A recommender, from GA4collect.recommender('similar').on(dl('view_item_list')).read(fromGa4.recommender);Delivery
Section titled “Delivery”- Each event is one request to
POST /analytics/v1/events, with the same browser token and visitor ID as Luma’s discovery requests. A failed request is retried once, with the sameevent_id. - Nothing is lost when the page changes. A click or add-to-cart on a Luma product is stored the moment it happens, and dataLayer pushes on a page the shopper leaves before analytics has loaded — a thank-you page closed at once — are kept. The next page in the same tab sends them, dated as they happened. A purchase is still sent once.
- Both depend on browser storage. Pending events are kept in
sessionStorage, so they are lost when the tab closes, or when storage is unavailable — a private window, a full quota. Sent purchase IDs are kept inlocalStorage; without it, a reloaded thank-you page sends its purchase again. - Nothing speculative counts. A prerendered page sends its events only once it is shown.
What it adds to the page
Section titled “What it adds to the page”Analytics loads as one file after the page has loaded, holding only what _analytics.ts
uses: every event, trigger and reader is separate, so fromGa4.purchase brings that reader
alone. Compressed sizes:
_analytics.ts | Size |
|---|---|
| None | Nothing loaded |
| Page views only | 2.2 kB |
| Page views, add-to-carts and purchases from GA4 | 3.6 kB |
+ Luma’s own clicks and add-to-carts (lumaClicks(), lumaCarts()) | +0.2 kB |
+ domClick() | +0.2 kB |
+ a fromGa4 list reader (.search, .autocomplete, .recommender) | +0.1 kB |
+ mutationOf(), xhrMatching() | +0.3 kB, +0.4 kB |
+ user.identify(), a page reference, a list read by your own function, .once(), .throttle() | Under 0.1 kB each |
Limitations
Section titled “Limitations”- A search shown in place fires no page view of yours.
lumaPages()counts it; your own tags do not. Seesearch.inPlaceQuery. - A shopper who leaves before analytics loads sends no page view, and no click on your
own elements (
domClick()). Clicks and add-to-carts on Luma’s products, and dataLayer pushes, are kept for the next page.
See also
Section titled “See also”- Analytics overview — what the events are for
- Event reference — the
interactionandtransactionfields - Visitor data — the visitor ID and what Luma stores in the browser
Was this page helpful?
Thanks.