@affiliateo/web 2.3.0 → 2.4.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/README.md CHANGED
@@ -171,12 +171,12 @@ attributes become metadata (kebab-case → snake_case, up to 10 params).
171
171
  </section>
172
172
  ```
173
173
 
174
- Fires once when 10% of the section enters the viewport (combined with a
175
- small bottom-edge rootMargin so it doesn't fire on sections barely peeking
176
- up from the bottom of the screen). The lower threshold catches tall
177
- sections like pricing pages with comparison tables that would otherwise
178
- top out under 50% intersection and never fire. Great for landing pages
179
- where you want to know which sections convert.
174
+ Fires the first time any pixel of the section overlaps the viewport.
175
+ Maximum permissiveness — if the user scrolled enough to see even a sliver,
176
+ it counts. Earlier versions required 50% visibility, which was unreachable
177
+ for tall sections (a pricing page with comparison tables silently dropped
178
+ out of every funnel). Great for landing pages where you want to know
179
+ which sections convert.
180
180
 
181
181
  ### Goal naming rules
182
182
 
package/dist/provider.js CHANGED
@@ -248,13 +248,33 @@ export function AffiliateoProvider({ campaignId, apiUrl, children, }) {
248
248
  ended = true;
249
249
  beacon({ type: 'session_end', campaign_id: campaignIdRef.current, visitor_id: vid, ref: ref || lsGet(LS_REF) || '' });
250
250
  }
251
+ // Server-side session window. Matches STALE_VISITOR_MS in the
252
+ // tracking Durable Object — after this much idle time the server
253
+ // treats the visitor as a new session, rotates session_id, and
254
+ // expects a fresh pageview to anchor the new visit in
255
+ // campaign_events. Without that anchor, funnel steps starting on
256
+ // `page` matches (e.g. the homepage `/`) silently get skipped for
257
+ // visitors who keep a tab open and come back.
258
+ const SESSION_REFIRE_MS = 10 * 60 * 1000;
251
259
  function onVisibilityChange() {
252
260
  if (document.visibilityState === 'hidden') {
253
261
  sendEnd();
254
262
  }
255
263
  else {
264
+ // Tab becomes visible again.
256
265
  ended = false;
257
- } // reset if they come back (tab switch)
266
+ // If the SDK's last pageview was longer ago than the server's
267
+ // 10-min session window, the server has already rolled the
268
+ // visitor over to a new session_id. Refire pageview so the new
269
+ // session has a proper anchor — otherwise the next scroll /
270
+ // click / custom event arrives orphaned and the funnel overlay
271
+ // sees the visitor enter "mid-funnel" (e.g. on a scroll step)
272
+ // without ever passing through the page-step that should
273
+ // bracket the visit.
274
+ if (Date.now() - lastSendTime > SESSION_REFIRE_MS) {
275
+ sendPageview();
276
+ }
277
+ }
258
278
  }
259
279
  // Send pageview on mount
260
280
  if (!sentRef.current) {
@@ -332,22 +352,16 @@ export function AffiliateoProvider({ campaignId, apiUrl, children, }) {
332
352
  // path) and unobserve after the first fire so the same element can't
333
353
  // double-count if the user scrolls back up.
334
354
  //
335
- // Threshold: 0.1 (was 0.5). 50% visibility is unreachable for tall
336
- // sections — a 1600px pricing section in an 800px viewport tops out
337
- // at ~50% intersection ratio, so the observer's threshold was either
338
- // a bare miss (section maxes at 49%) or a brittle hit. Funnel steps
339
- // tied to those sections silently dropped from every merchant's
340
- // conversion count without anyone noticing the section was even
341
- // being rendered. 0.1 fires when 10% of the section is visible,
342
- // which on a typical laptop is "the user has scrolled enough that
343
- // the section is meaningfully on screen" without firing on sections
344
- // that barely peek up from the bottom edge.
345
- //
346
- // rootMargin: trim the bottom 10% of the viewport from the trigger
347
- // zone so a section creeping up from the very bottom of the screen
348
- // doesn't fire instantly. The user has to scroll enough that the
349
- // section is starting to settle into the main visible area, which
350
- // matches a human's intuitive sense of "I've reached that section."
355
+ // No options object → IntersectionObserver defaults to
356
+ // { threshold: 0, rootMargin: '0px 0px 0px 0px' }, which fires the
357
+ // instant ANY pixel of the section overlaps the viewport. That's
358
+ // exactly the semantic merchants want for "did the visitor reach
359
+ // this section": if they scrolled enough to see even a sliver of
360
+ // it, count it. Earlier versions used { threshold: 0.5 }, which
361
+ // was unreachable for tall sections (a 1600px pricing section in
362
+ // an 800px viewport tops out at ~50% intersection ratio and never
363
+ // crossed the threshold) — funnel steps tied to those sections
364
+ // silently dropped from every merchant's conversion count.
351
365
  const scrollFired = new WeakSet();
352
366
  let scrollObserver = null;
353
367
  let scrollMo = null;
@@ -364,7 +378,7 @@ export function AffiliateoProvider({ campaignId, apiUrl, children, }) {
364
378
  track(goal);
365
379
  scrollObserver?.unobserve(entry.target);
366
380
  }
367
- }, { threshold: 0.1, rootMargin: '0px 0px -10% 0px' });
381
+ });
368
382
  // Observe everything that exists at mount time.
369
383
  document.querySelectorAll('[data-affiliateo-scroll]').forEach((el) => {
370
384
  scrollObserver?.observe(el);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@affiliateo/web",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Affiliateo web SDK — affiliate attribution and tracking for web apps",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",