fansunited-frontend-components 0.0.74 → 0.0.76
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/Predictor.js +2 -18905
- package/README.md +220 -1
- package/chunks/Predictor-ayYmCcwm.js +19265 -0
- package/index.d.ts +2 -0
- package/index.d.ts.map +1 -1
- package/index.js +25 -24
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -237,6 +237,179 @@ import { ClassicQuizPlay } from "fansunited-frontend-components";
|
|
|
237
237
|
/>
|
|
238
238
|
```
|
|
239
239
|
|
|
240
|
+
## Analytics
|
|
241
|
+
|
|
242
|
+
`Predictor` emits a typed event for every meaningful step of the user journey — viewed, started a
|
|
243
|
+
prediction, submitted, joined a private league, clicked an odd. Use them for funnels, conversion
|
|
244
|
+
goals and remarketing audiences.
|
|
245
|
+
|
|
246
|
+
> Currently `Predictor` only. The bus is component-agnostic, so other components can join without a
|
|
247
|
+
> breaking change. This is separate from the `onFinish` / `onShare` callbacks above.
|
|
248
|
+
|
|
249
|
+
### Two ways to consume
|
|
250
|
+
|
|
251
|
+
Both receive the identical stream, and can be used together.
|
|
252
|
+
|
|
253
|
+
**1. The `callbacks` prop** — for when you own the JSX:
|
|
254
|
+
|
|
255
|
+
```tsx
|
|
256
|
+
<Predictor
|
|
257
|
+
entityId="predictor-template-123"
|
|
258
|
+
sdk={sdk}
|
|
259
|
+
language="en"
|
|
260
|
+
callbacks={{
|
|
261
|
+
onEvent: (event) => analytics.track(event.name, { ...event.params, ...event.context }),
|
|
262
|
+
}}
|
|
263
|
+
/>
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**2. The `componentEvents` bus** — for when you don't: a tag manager, a separately-bundled script,
|
|
267
|
+
anything that loads after the component mounts.
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
import { componentEvents } from "fansunited-frontend-components";
|
|
271
|
+
|
|
272
|
+
const unsubscribe = componentEvents.subscribe((event) => {
|
|
273
|
+
console.log(event.component, event.name, event.params);
|
|
274
|
+
});
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
`subscribe` returns an unsubscribe function, and **replays recent events** to a new subscriber (the
|
|
278
|
+
last 50, within the last 5 minutes) — so a listener that attaches after mount still sees
|
|
279
|
+
`widget_view` and anything else it missed. Without that, funnel denominators under-count.
|
|
280
|
+
|
|
281
|
+
The bus is subscribe-only by design. There is no public way to emit, so nothing else on the page can
|
|
282
|
+
forge events into your analytics.
|
|
283
|
+
|
|
284
|
+
### Event shape
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
type ComponentEventParams = Record<string, string | number | boolean | null>;
|
|
288
|
+
|
|
289
|
+
interface PredictorComponentEvent {
|
|
290
|
+
component: "predictor";
|
|
291
|
+
name: PredictorAnalyticsEventName;
|
|
292
|
+
params: ComponentEventParams;
|
|
293
|
+
context: PredictorAnalyticsContext;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Discriminated on `component`. One member today. */
|
|
297
|
+
type ComponentEvent = PredictorComponentEvent;
|
|
298
|
+
|
|
299
|
+
interface PredictorAnalyticsContext {
|
|
300
|
+
entityId: string;
|
|
301
|
+
template: "standard" | "embed";
|
|
302
|
+
language: LanguageType;
|
|
303
|
+
userIsLoggedIn: boolean;
|
|
304
|
+
templateTitle: string | null;
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
`context` is identical on every event from an instance; `params` are per-event. Narrow on
|
|
309
|
+
`component` before reading them — when a second component starts emitting, the union widens and code
|
|
310
|
+
that already narrows keeps compiling:
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
componentEvents.subscribe((event) => {
|
|
314
|
+
if (event.component !== "predictor") return;
|
|
315
|
+
event.context.entityId; // typed here
|
|
316
|
+
});
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Three properties you can rely on:
|
|
320
|
+
|
|
321
|
+
- **Flat primitives only.** No nested objects or arrays; multi-value fields are comma-joined
|
|
322
|
+
strings, because tag managers and GA4 handle array parameters poorly.
|
|
323
|
+
- **Every key an event defines is always present**, `null` rather than absent.
|
|
324
|
+
- **No personal data.** No profile id, email, display name, token, or private-league invitation
|
|
325
|
+
code. The only user-level fact is `context.userIsLoggedIn`.
|
|
326
|
+
|
|
327
|
+
### Events
|
|
328
|
+
|
|
329
|
+
Every event also carries `context`.
|
|
330
|
+
|
|
331
|
+
| Group | Event | Params |
|
|
332
|
+
| --- | --- | --- |
|
|
333
|
+
| Lifecycle | `widget_view` | — |
|
|
334
|
+
| | `tab_view` | `tab` |
|
|
335
|
+
| | `matchweek_change` | `direction`, `group_id` |
|
|
336
|
+
| Prediction | `prediction_started` | match descriptors + `market` |
|
|
337
|
+
| | `prediction_submitted` | match descriptors + `markets`, `market_count`, `is_edit` |
|
|
338
|
+
| | `prediction_failed` | match descriptors + `markets`, `market_count`, `is_edit` |
|
|
339
|
+
| | `prediction_edited` | match descriptors + `market` |
|
|
340
|
+
| | `prediction_removed` | match descriptors + `market` |
|
|
341
|
+
| Gates | `sign_in_prompt_shown` | match descriptors + `trigger` |
|
|
342
|
+
| | `sign_in_click` | `location` |
|
|
343
|
+
| | `consent_gate_shown` / `consent_accepted` / `consent_declined` | `consent_ids` |
|
|
344
|
+
| Engagement | `odds_click` | `match_id`, `outcome`, `odd_value`, `operator` |
|
|
345
|
+
| | `leaderboard_page_change` | `page`, `total_pages` |
|
|
346
|
+
| | `embed_success_cta_click` | `match_id`, `cta_label`, `url` |
|
|
347
|
+
| Private leagues | `private_league_created` / `_joined` / `_view` / `_invite_shared` / `_invitation_accepted` / `_invitation_declined` / `_left` / `_deleted` | `league_id`, `league_name` |
|
|
348
|
+
|
|
349
|
+
**Match descriptors** are `match_id`, `home_team`, `home_team_id`, `away_team`, `away_team_id`,
|
|
350
|
+
`kickoff_at`.
|
|
351
|
+
|
|
352
|
+
**Join on the ids, not the names.** `match_id` and the team ids are canonical, provider-agnostic
|
|
353
|
+
identifiers (`fb:m:*`, `fb:t:*`) — stable across languages and across whichever football data
|
|
354
|
+
provider is behind the account. `match_id` is the same value the prediction itself is posted with,
|
|
355
|
+
so an event joins 1:1 with its prediction record. The `home_team` / `away_team` names are
|
|
356
|
+
**localised by the `language` prop** and exist for readable reports only.
|
|
357
|
+
|
|
358
|
+
Notes on firing:
|
|
359
|
+
|
|
360
|
+
- `widget_view` fires once per `entityId`, held until the template request settles — before that the
|
|
361
|
+
component is a skeleton.
|
|
362
|
+
- `prediction_started` fires once per match, on the user's first answer, however much they then
|
|
363
|
+
change it.
|
|
364
|
+
- A partially-failed multi-market submit emits **both** `prediction_submitted` and
|
|
365
|
+
`prediction_failed`, each listing only its own markets.
|
|
366
|
+
- `sign_in_click` does not fire for a `signInCTA.component` you supply — that component owns its own
|
|
367
|
+
clicks. Where it does fire, your `signInCTA.onClick` still runs.
|
|
368
|
+
|
|
369
|
+
### Wiring to a tag manager
|
|
370
|
+
|
|
371
|
+
Every component renders inside a **shadow root**, so a tag manager's built-in no-code triggers (all
|
|
372
|
+
clicks, form submission, element visibility) cannot see into it: a click arrives at `document`
|
|
373
|
+
retargeted to the shadow host, giving one anonymous `<div>` with no text, classes or id, and native
|
|
374
|
+
`submit` events don't escape a shadow root at all. That's inherent to the CSS isolation, not a
|
|
375
|
+
configuration mistake — pushing named events is the answer, and it's more stable anyway.
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
import { componentEvents } from "fansunited-frontend-components";
|
|
379
|
+
|
|
380
|
+
window.dataLayer = window.dataLayer || [];
|
|
381
|
+
|
|
382
|
+
componentEvents.subscribe((event) => {
|
|
383
|
+
window.dataLayer.push({
|
|
384
|
+
event: `fu_${event.name}`,
|
|
385
|
+
...event.params,
|
|
386
|
+
...event.context,
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Then create a Custom Event trigger on `fu_prediction_submitted`, a GA4 Event tag, and Data Layer
|
|
392
|
+
Variables for the parameters you want. A single trigger matching `^fu_` with `{{Event}}` as the tag's
|
|
393
|
+
event name forwards the whole catalogue at once.
|
|
394
|
+
|
|
395
|
+
Two things to get right:
|
|
396
|
+
|
|
397
|
+
- **Prefix the event names.** `dataLayer` is shared with everything else on the page. The library
|
|
398
|
+
deliberately does not prefix for you — that would bake its naming into your container.
|
|
399
|
+
- **GTM merges pushes rather than replacing them.** If one push sets `match_id` and a later event
|
|
400
|
+
omits that key, GTM still reports the old value. Either clear the keys an event doesn't define, or
|
|
401
|
+
nest everything under one key and read it with dot notation:
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
window.dataLayer.push({ event: `fu_${event.name}`, fu: { ...event.params, ...event.context } });
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Verify with:
|
|
408
|
+
|
|
409
|
+
```js
|
|
410
|
+
window.dataLayer.filter((e) => String(e.event).startsWith("fu_"));
|
|
411
|
+
```
|
|
412
|
+
|
|
240
413
|
## Components
|
|
241
414
|
|
|
242
415
|
### ClassicQuizPlay
|
|
@@ -3460,6 +3633,7 @@ Football score predictor component with two templates: the default **multi-tab**
|
|
|
3460
3633
|
| `consents` | `ConsentDef[]` | Consent definitions required before predicting |
|
|
3461
3634
|
| `matchCardBgImageUrl` | `string` | Background image URL for match prediction cards. Ignored by `"embed"` |
|
|
3462
3635
|
| `betslip` | `PredictorBetslipConfig` | Betslip integration config. Omit to disable. See [Betslip Integration](#betslip-integration) |
|
|
3636
|
+
| `callbacks` | `PredictorCallbacks` | `onEvent` — one observer for every journey event the component emits. See [Analytics](#analytics) |
|
|
3463
3637
|
|
|
3464
3638
|
#### Predictor Templates
|
|
3465
3639
|
|
|
@@ -3763,6 +3937,37 @@ type PredictorBetslipTrigger =
|
|
|
3763
3937
|
|
|
3764
3938
|
**Theme fallback:** If `betslip.themeOptions` is not provided, the Betslip widget inherits the Predictor's own `themeOptions`. A single `themeOptions` on the Predictor is enough to theme both components consistently.
|
|
3765
3939
|
|
|
3940
|
+
#### Analytics
|
|
3941
|
+
|
|
3942
|
+
The Predictor emits a typed event for every meaningful step — `widget_view`, `tab_view`,
|
|
3943
|
+
`prediction_started`, `prediction_submitted`, `private_league_joined`, `odds_click` and more.
|
|
3944
|
+
|
|
3945
|
+
```tsx
|
|
3946
|
+
<Predictor
|
|
3947
|
+
entityId="predictor-template-123"
|
|
3948
|
+
sdk={sdk}
|
|
3949
|
+
language="en"
|
|
3950
|
+
callbacks={{
|
|
3951
|
+
onEvent: (event) => analytics.track(event.name, { ...event.params, ...event.context }),
|
|
3952
|
+
}}
|
|
3953
|
+
/>
|
|
3954
|
+
```
|
|
3955
|
+
|
|
3956
|
+
The same stream is available without a prop, via the `componentEvents` bus — which is what you want
|
|
3957
|
+
for a tag manager, since it can subscribe after the component has already mounted:
|
|
3958
|
+
|
|
3959
|
+
```ts
|
|
3960
|
+
import { componentEvents } from "fansunited-frontend-components";
|
|
3961
|
+
|
|
3962
|
+
componentEvents.subscribe((event) => {
|
|
3963
|
+
window.dataLayer.push({ event: `fu_${event.name}`, ...event.params, ...event.context });
|
|
3964
|
+
});
|
|
3965
|
+
```
|
|
3966
|
+
|
|
3967
|
+
Payloads carry no personal data. Full event catalogue, parameter tables, the join-key rules, and the
|
|
3968
|
+
tag-manager setup (including why built-in click triggers cannot see into the component's shadow root)
|
|
3969
|
+
are in [Analytics](#analytics).
|
|
3970
|
+
|
|
3766
3971
|
#### TypeScript Support
|
|
3767
3972
|
|
|
3768
3973
|
Import types from `fansunited-frontend-core`:
|
|
@@ -3783,9 +3988,23 @@ import {
|
|
|
3783
3988
|
OnSuccessCTADetails,
|
|
3784
3989
|
CustomThemeOptions,
|
|
3785
3990
|
LanguageType,
|
|
3991
|
+
// Analytics
|
|
3992
|
+
PredictorCallbacks,
|
|
3993
|
+
ComponentEvent,
|
|
3994
|
+
ComponentEventParams,
|
|
3995
|
+
PredictorComponentEvent,
|
|
3996
|
+
PredictorAnalyticsContext,
|
|
3997
|
+
PredictorAnalyticsEventName,
|
|
3786
3998
|
} from "fansunited-frontend-core";
|
|
3787
3999
|
```
|
|
3788
4000
|
|
|
4001
|
+
The `componentEvents` bus itself is imported from `fansunited-frontend-components`, not from core:
|
|
4002
|
+
|
|
4003
|
+
```tsx
|
|
4004
|
+
import { componentEvents } from "fansunited-frontend-components";
|
|
4005
|
+
import type { ComponentEventsApi } from "fansunited-frontend-components";
|
|
4006
|
+
```
|
|
4007
|
+
|
|
3789
4008
|
#### Examples
|
|
3790
4009
|
|
|
3791
4010
|
##### Basic Predictor
|
|
@@ -5173,7 +5392,7 @@ This package exports the following components:
|
|
|
5173
5392
|
- **`EitherOrPlay`** - Binary choice game with standings panel and customizable templates
|
|
5174
5393
|
- **`Discussion`** - Threaded comments with reactions, replies, reporting, and authentication support
|
|
5175
5394
|
- **`Leaderboard`** - Ranked standings for Classic Quiz, Match Quiz, Top X, and Template entities with multi-entity support
|
|
5176
|
-
- **`Predictor`** - Multi-tab football score predictor with leaderboard, private leagues, rules, and
|
|
5395
|
+
- **`Predictor`** - Multi-tab football score predictor with leaderboard, private leagues, rules, prizes, and a journey-event analytics bus
|
|
5177
5396
|
- **`TopX`** - Multi-tab competitive picks predictor with game navigation, leaderboard, private leagues, rules, and prizes
|
|
5178
5397
|
- **`Betslip`** - Floating betslip with bookmaker deep-link integration, six viewport positions, and command bus API
|
|
5179
5398
|
- **`Standings`** - Sport-agnostic competition standings table with zone highlighting, rank movement, and team form
|