@autobusal/providers 1.23.0 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/Setup/commerce.ts +138 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.24.0
|
|
4
|
+
|
|
5
|
+
**New `Setup/commerce.ts` — the booking funnel, pushed to the dataLayer.**
|
|
6
|
+
|
|
7
|
+
`analytics.ts` already loaded the container and announced page views; this is what makes those page views mean something. Until now the funnel was unmeasurable for a more basic reason than missing events — every step of the wizard shared one URL, so no tool could tell a search from a checkout.
|
|
8
|
+
|
|
9
|
+
Uses **GA4's own event names** (`view_item_list`, `select_item`, `begin_checkout`, `add_to_cart`, `remove_from_cart`, `purchase`) so a container wires them by picking from a list rather than needing a custom mapping per brand.
|
|
10
|
+
|
|
11
|
+
`purchase` is the hard one and the reason this is a module rather than a few inline pushes: a card payment leaves the site entirely and returns on a URL carrying nothing but an order hash, and most buyers are guests with no account to read the order back from. So `stash()` remembers the value at order time and `flush()` announces it on return — clearing as it fires, and checking the hash, so neither a refresh nor a stale entry from an abandoned attempt can report a sale.
|
|
12
|
+
|
|
13
|
+
Each push clears `ecommerce` first; GTM's dataLayer is cumulative, and without that a later event inherits the previous one's items.
|
|
14
|
+
|
|
3
15
|
## 1.23.0
|
|
4
16
|
|
|
5
17
|
- `CountryData.timezone` — the IANA zone, so the date picker can floor itself on the departure city's date rather than ours or the buyer's. Carried on `/api/cities/browse`, which the search form already loads.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The booking funnel, pushed to the dataLayer.
|
|
3
|
+
*
|
|
4
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-05
|
|
5
|
+
*
|
|
6
|
+
* `analytics.ts` loads the container and announces page views; this is what
|
|
7
|
+
* makes those page views mean something. Until now the funnel was
|
|
8
|
+
* unmeasurable for a more basic reason than missing events - every step of
|
|
9
|
+
* the wizard shared one URL, so no tool could tell a search from a checkout.
|
|
10
|
+
* That is fixed; this is the other half.
|
|
11
|
+
*
|
|
12
|
+
* GA4'S OWN EVENT NAMES, deliberately. `view_item_list`, `select_item`,
|
|
13
|
+
* `begin_checkout`, `add_to_cart` and `purchase` are wired in GTM by
|
|
14
|
+
* choosing them from a list; anything invented here would need a custom
|
|
15
|
+
* mapping in every brand's container, and nothing in this codebase should
|
|
16
|
+
* know which tags a brand runs.
|
|
17
|
+
*
|
|
18
|
+
* PURCHASE IS THE HARD ONE, and the reason this file exists rather than a
|
|
19
|
+
* few inline pushes. A card payment leaves the site entirely and comes back
|
|
20
|
+
* on a URL carrying nothing but an order hash, and most buyers are guests
|
|
21
|
+
* with no account to look the order up with. So the value is stashed when
|
|
22
|
+
* the order is placed and flushed when the buyer returns - see stash() and
|
|
23
|
+
* flush() below.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const push = (payload: Record<string, unknown>): void => {
|
|
27
|
+
window.dataLayer = window.dataLayer || [];
|
|
28
|
+
|
|
29
|
+
// Clear the previous event's ecommerce object first. GTM's dataLayer is
|
|
30
|
+
// cumulative, so without this a later event inherits whatever items the
|
|
31
|
+
// last one left behind - the classic double-counted purchase.
|
|
32
|
+
window.dataLayer.push({ ecommerce: null });
|
|
33
|
+
window.dataLayer.push(payload);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export interface CommerceItem {
|
|
37
|
+
item_id: string
|
|
38
|
+
item_name: string
|
|
39
|
+
item_brand?: string
|
|
40
|
+
price?: number
|
|
41
|
+
quantity?: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Search results were shown. */
|
|
45
|
+
export const viewItemList = (items: CommerceItem[], currency?: string): void => {
|
|
46
|
+
if (items.length === 0) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
push({ event: 'view_item_list', ecommerce: { currency, items } });
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** One coach was chosen from the results. */
|
|
54
|
+
export const selectItem = (item: CommerceItem, currency?: string): void => {
|
|
55
|
+
push({ event: 'select_item', ecommerce: { currency, items: [item] } });
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** The buyer reached the passenger-details and payment screen. */
|
|
59
|
+
export const beginCheckout = (value: number, items: CommerceItem[], currency?: string): void => {
|
|
60
|
+
push({ event: 'begin_checkout', ecommerce: { currency, value, items } });
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A paid add-on was ticked or unticked.
|
|
65
|
+
*
|
|
66
|
+
* The attach rate this measures is the single input the Flexible Ticket
|
|
67
|
+
* pricing decision is waiting on - it cannot be banded by window without
|
|
68
|
+
* knowing how often each window is actually bought.
|
|
69
|
+
*/
|
|
70
|
+
export const addon = (added: boolean, name: string, price: number, currency?: string): void => {
|
|
71
|
+
push({
|
|
72
|
+
event: added ? 'add_to_cart' : 'remove_from_cart',
|
|
73
|
+
ecommerce: {
|
|
74
|
+
currency,
|
|
75
|
+
value: price,
|
|
76
|
+
items: [{ item_id: name, item_name: name, price, quantity: 1 }]
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const KEY = 'commerce.purchase';
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Remember what an order was worth, against its hash.
|
|
85
|
+
*
|
|
86
|
+
* sessionStorage rather than a variable: a card payment navigates away to
|
|
87
|
+
* the bank and back, so anything held in memory is gone by the time the
|
|
88
|
+
* buyer returns. Same origin either side, so the value survives.
|
|
89
|
+
*/
|
|
90
|
+
export const stash = (hash: string, value: number, items: CommerceItem[], currency?: string): void => {
|
|
91
|
+
try {
|
|
92
|
+
sessionStorage.setItem(KEY, JSON.stringify({ hash, value, items, currency }));
|
|
93
|
+
} catch {
|
|
94
|
+
// storage disabled or full - measurement is never worth breaking a sale
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Announce a completed purchase, once.
|
|
100
|
+
*
|
|
101
|
+
* CLEARED AS IT FIRES. The confirmation page is a plain URL a buyer can
|
|
102
|
+
* refresh, bookmark or reach twice from their history, and each of those
|
|
103
|
+
* would otherwise report another sale. The hash is checked too, so a stale
|
|
104
|
+
* entry from an abandoned attempt cannot be attributed to a later order.
|
|
105
|
+
*/
|
|
106
|
+
export const flush = (hash: string): void => {
|
|
107
|
+
let stored: { hash?: string, value?: number, items?: CommerceItem[], currency?: string } | null = null;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const raw = sessionStorage.getItem(KEY);
|
|
111
|
+
|
|
112
|
+
stored = raw ? JSON.parse(raw) : null;
|
|
113
|
+
} catch {
|
|
114
|
+
stored = null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!stored || stored.hash !== hash) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
sessionStorage.removeItem(KEY);
|
|
123
|
+
} catch {
|
|
124
|
+
// if it cannot be cleared, do not fire - a repeatable purchase event is
|
|
125
|
+
// worse than a missing one
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
push({
|
|
130
|
+
event: 'purchase',
|
|
131
|
+
ecommerce: {
|
|
132
|
+
transaction_id: hash,
|
|
133
|
+
currency: stored.currency,
|
|
134
|
+
value: stored.value,
|
|
135
|
+
items: stored.items ?? []
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
};
|