SPB Git

spb/ka-ui

Public
7.4 KB · 224 lines javascript
1/* Groupe KA — ka-id.js : SDK client KA ID v2 (personnalisation).2 * SOURCE CANONIQUE : ka-ui.git/kaid/ka-id.js — copié dans frontend/public/3 * (ou public/) de chaque app par sync-kaid.sh. Corriger ICI puis redistribuer.4 *5 * Rôle : signaux navigateur que le serveur ne voit pas — position des clics6 * dans les résultats, temps passé sur une fiche (dwell), visites de retour —7 * plus les aides d'interface : KAID.hide() (« Pas pour moi »),8 * KAID.saveSearch() (recherche sauvegardée), badge « Recommandé pour vous ».9 *10 * Intégration (index.html, AVANT le bundle) :11 *   <script>12 *     window.KAID_CONF = {13 *       card:   "a[href^='/emploi/']",      // sélecteur des cartes résultat14 *       detail: "^/emploi/(.+)$",           // regex fiche → capture l'uid15 *       entity: "job",                      // entity_type des événements16 *     };17 *   </script>18 *   <script src="/ka-id.js" defer></script>19 *20 * Vie privée : ne fait RIEN si l'utilisateur n'est pas connecté via KA ID21 * (vérifié auprès de /api/kaid/status) ; le hub respecte ensuite ses22 * réglages Historique/Personnalisation (voir groupe-ka.com/mon-ka).23 */24(function () {25  "use strict";26  var CONF = window.KAID_CONF || {};27  var API = CONF.api || "/api/kaid";28  var connected = null;          // null = inconnu, sinon bool29  var queue = [];30  var flushTimer = null;3132  /* ---------- transport (lots de 10, sendBeacon à la fermeture) ---------- */3334  function post(events, useBeacon) {35    if (!events.length) return;36    var body = JSON.stringify({ events: events });37    if (useBeacon && navigator.sendBeacon) {38      navigator.sendBeacon(API + "/events",39        new Blob([body], { type: "application/json" }));40      return;41    }42    fetch(API + "/events", {43      method: "POST",44      headers: { "Content-Type": "application/json" },45      body: body,46      keepalive: true,47      credentials: "same-origin",48    }).catch(function () {});49  }5051  function flush(useBeacon) {52    if (!queue.length) return;53    var batch = queue.splice(0, queue.length);54    post(batch, useBeacon);55  }5657  function track(type, data) {58    if (connected === false) return;59    var e = { type: type };60    if (data) for (var k in data) if (data[k] != null) e[k] = data[k];61    queue.push(e);62    if (queue.length >= 10) flush(false);63    else if (!flushTimer)64      flushTimer = setTimeout(function () { flushTimer = null; flush(false); }, 4000);65  }6667  /* ---------- connexion ---------- */6869  function init(cb) {70    try {71      var cached = sessionStorage.getItem("kaid_status");72      if (cached) {73        var c = JSON.parse(cached);74        if (Date.now() - c.t < 600000) { connected = c.on; cb(); return; }75      }76    } catch (err) { /* stockage indisponible */ }77    fetch(API + "/status", { credentials: "same-origin" })78      .then(function (r) { return r.json(); })79      .then(function (d) {80        connected = !!d.connected;81        try {82          sessionStorage.setItem("kaid_status",83            JSON.stringify({ on: connected, t: Date.now() }));84        } catch (err) { /* stockage indisponible */ }85        cb();86      })87      .catch(function () { connected = false; });88  }8990  /* ---------- clics sur les cartes résultat ---------- */9192  function uidFromHref(el) {93    if (!CONF.detail) return null;94    var href = el.getAttribute("href") || "";95    var path = href.replace(/^https?:\/\/[^/]+/, "").split("?")[0];96    var m = path.match(new RegExp(CONF.detail));97    return m ? decodeURIComponent(m[1]) : null;98  }99100  function watchClicks() {101    if (!CONF.card) return;102    document.addEventListener("click", function (ev) {103      var el = ev.target && ev.target.closest && ev.target.closest(CONF.card);104      if (!el) return;105      var uid = (CONF.uid ? CONF.uid(el) : null) || uidFromHref(el);106      if (!uid) return;107      var cards = document.querySelectorAll(CONF.card);108      var pos = Array.prototype.indexOf.call(cards, el);109      track("click", {110        entity_type: CONF.entity, entity_id: uid,111        position: pos >= 0 ? pos : null,112      });113    }, true);114  }115116  /* ---------- temps passé sur une fiche (dwell) ---------- */117118  var dwell = { uid: null, t0: 0 };119120  function detailUid() {121    if (!CONF.detail) return null;122    var m = location.pathname.match(new RegExp(CONF.detail));123    return m ? decodeURIComponent(m[1]) : null;124  }125126  function endDwell(useBeacon) {127    if (!dwell.uid) return;128    var ms = Date.now() - dwell.t0;129    if (ms >= 8000)130      track("detail_dwell",131        { entity_type: CONF.entity, entity_id: dwell.uid, dwell_ms: ms });132    dwell.uid = null;133    if (useBeacon) flush(true);134  }135136  function checkRoute() {137    var uid = detailUid();138    if (uid === dwell.uid) return;139    endDwell(false);140    if (uid) dwell = { uid: uid, t0: Date.now() };141  }142143  function watchRoutes() {144    ["pushState", "replaceState"].forEach(function (fn) {145      var orig = history[fn];146      history[fn] = function () {147        var out = orig.apply(this, arguments);148        setTimeout(checkRoute, 50);149        return out;150      };151    });152    window.addEventListener("popstate", function () { setTimeout(checkRoute, 50); });153    document.addEventListener("visibilitychange", function () {154      if (document.visibilityState === "hidden") { endDwell(true); }155      else checkRoute();156    });157    window.addEventListener("pagehide", function () { endDwell(true); });158    checkRoute();159  }160161  /* ---------- visite de retour ---------- */162163  function returnVisit() {164    try {165      var last = Number(localStorage.getItem("kaid_last") || 0);166      var now = Date.now();167      if (last && now - last > 72e6) track("return_visit", {}); // > 20 h168      localStorage.setItem("kaid_last", String(now));169    } catch (err) { /* stockage indisponible */ }170  }171172  /* ---------- API publique ---------- */173174  window.KAID = {175    track: track,176    connected: function () { return connected; },177    /** « Pas pour moi » — masque l'annonce partout et nourrit le signal négatif. */178    hide: function (itemId, features) {179      return fetch(API + "/hide", {180        method: "POST",181        headers: { "Content-Type": "application/json" },182        credentials: "same-origin",183        body: JSON.stringify({ item_id: itemId, on: true, features: features || null }),184      }).then(function (r) { return r.json(); });185    },186    /** Sauvegarde la recherche courante (par défaut : URL + titre du document). */187    saveSearch: function (opts) {188      opts = opts || {};189      return fetch(API + "/saved-searches", {190        method: "POST",191        headers: { "Content-Type": "application/json" },192        credentials: "same-origin",193        body: JSON.stringify({194          action: "add",195          label: opts.label || document.title.split("|")[0].split("—")[0].trim(),196          query: opts.query || null,197          filters: opts.filters || null,198          url: opts.url || (location.origin + location.pathname + location.search),199          alert: !!opts.alert,200          frequency: opts.frequency || null,201        }),202      }).then(function (r) { return r.json(); });203    },204    external: function (itemId, features) {205      track("external_click",206        { entity_type: CONF.entity, entity_id: itemId, features: features });207      flush(true);208    },209  };210211  /* ---------- démarrage ---------- */212213  function start() {214    if (!connected) return;215    watchClicks();216    watchRoutes();217    returnVisit();218  }219220  if (document.readyState === "loading")221    document.addEventListener("DOMContentLoaded", function () { init(start); });222  else init(start);223})();224
Files · a0df7014a9101c4d24d9fe8174277bfc79fd8cda
Menu

Theme