HannesVonEssen commited on
Commit
b35e2da
·
verified ·
1 Parent(s): 7f00c04

Harden Space analytics attribution and privacy

Browse files
Files changed (3) hide show
  1. hf-shell-app.js +129 -2
  2. index.html +124 -9
  3. space-auth.js +71 -1
hf-shell-app.js CHANGED
@@ -1275,6 +1275,28 @@
1275
  "hfviewer_actor",
1276
  ];
1277
  const TRACKING_ACTOR_LABEL_QUERY_KEY = "tracking_actor_label";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1278
  const HFVIEWER_INTERNAL_TRACKING_STORAGE_KEY = "hfviewer:internalTracking:v1";
1279
  const HFVIEWER_INTERNAL_TRACKING_COOKIE_NAME = "hfviewer_internal";
1280
  const HFVIEWER_INTERNAL_ANALYTICS_EMAILS = new Set([
@@ -6729,6 +6751,7 @@
6729
  TRACKING_ACTOR_QUERY_KEYS.forEach((key) => url.searchParams.delete(key));
6730
  url.searchParams.delete(TRACKING_ACTOR_LABEL_QUERY_KEY);
6731
  url.searchParams.delete("codex_ts");
 
6732
  return url;
6733
  }
6734
 
@@ -6749,6 +6772,56 @@
6749
  }
6750
  }
6751
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6752
  const HFVIEWER_FIRST_TOUCH_ATTRIBUTION_KEY = "hfviewer:firstTouchAttribution:v1";
6753
  const HFVIEWER_ATTRIBUTION_VISIT_ID_KEY = "hfviewer:attributionVisitId:v1";
6754
  const ATTRIBUTION_QUERY_KEYS = [
@@ -6765,6 +6838,8 @@
6765
  "ttclid",
6766
  "li_fat_id",
6767
  "mcp_token",
 
 
6768
  ];
6769
  let cachedAttributionPayload = null;
6770
 
@@ -6903,6 +6978,7 @@
6903
  page_title: document.title,
6904
  page_path: sanitizedAnalyticsPagePath(),
6905
  page_location: sanitizedAnalyticsUrl(),
 
6906
  ...params,
6907
  ...(tracking
6908
  ? {
@@ -7197,6 +7273,23 @@
7197
  hfviewer_hf_name: identity.name || undefined,
7198
  hfviewer_last_identified_source: source || "hf_oauth_session",
7199
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7200
  }
7201
  trackPendingHfOauthReturnFromSession(payload, source);
7202
  return internalMatch;
@@ -7368,6 +7461,9 @@
7368
  ads_allowed: hasAdsConsent,
7369
  allowed_all: Boolean(consent?.allowed),
7370
  };
 
 
 
7371
  const categories = consent?.categories || {};
7372
  const categoryEntries = Object.entries(categories)
7373
  .filter(([, value]) => typeof value === "boolean")
@@ -7511,7 +7607,7 @@
7511
  }
7512
  } catch {}
7513
  }
7514
- return event;
7515
  },
7516
  disable_session_recording:
7517
  disableMobileSessionRecording || Boolean(internalAutomationLabel),
@@ -7533,6 +7629,7 @@
7533
  window.posthog.register?.({
7534
  hfviewer_surface: "top_level_shell",
7535
  is_visualizer_frame: false,
 
7536
  anonymous_persistence_scope: latestAnalyticsConsentState.analytics_allowed
7537
  ? "durable_consented"
7538
  : "session_tab",
@@ -7886,6 +7983,36 @@
7886
  // and property sanitizer as the rest of the first-party shell.
7887
  window.hfviewerTrackPostHogEvent = trackPostHogEvent;
7888
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7889
  function identifyPostHogUserByEmail(email, personProperties = {}) {
7890
  const normalizedEmail = normalizeAnalyticsEmail(email);
7891
  if (!normalizedEmail || shouldSuppressPostHogForInternalTracking()) {
@@ -58606,4 +58733,4 @@ ${shouldInstallBridge ? `<script>${bridgeScript}</scr${"ipt"}>` : ""}
58606
  scheduleCommunityShowcasePermalinkScroll();
58607
  });
58608
  syncLandingModelCardButton();
58609
-
 
1275
  "hfviewer_actor",
1276
  ];
1277
  const TRACKING_ACTOR_LABEL_QUERY_KEY = "tracking_actor_label";
1278
+ const SENSITIVE_ANALYTICS_QUERY_KEYS = new Set([
1279
+ "__sign",
1280
+ "access_token",
1281
+ "auth_token",
1282
+ "code",
1283
+ "id_token",
1284
+ "state",
1285
+ "token",
1286
+ ]);
1287
+ const HFVIEWER_PRODUCT_CONTEXT = (() => {
1288
+ const configured =
1289
+ window.HFVIEWER_PRODUCT_CONTEXT &&
1290
+ typeof window.HFVIEWER_PRODUCT_CONTEXT === "object"
1291
+ ? window.HFVIEWER_PRODUCT_CONTEXT
1292
+ : {};
1293
+ const text = (value, maxLength = 120) => `${value || ""}`.trim().slice(0, maxLength);
1294
+ return Object.freeze({
1295
+ product_surface: text(configured.product_surface) || "website",
1296
+ space_repo: text(configured.space_repo),
1297
+ space_revision: text(configured.space_revision),
1298
+ });
1299
+ })();
1300
  const HFVIEWER_INTERNAL_TRACKING_STORAGE_KEY = "hfviewer:internalTracking:v1";
1301
  const HFVIEWER_INTERNAL_TRACKING_COOKIE_NAME = "hfviewer_internal";
1302
  const HFVIEWER_INTERNAL_ANALYTICS_EMAILS = new Set([
 
6751
  TRACKING_ACTOR_QUERY_KEYS.forEach((key) => url.searchParams.delete(key));
6752
  url.searchParams.delete(TRACKING_ACTOR_LABEL_QUERY_KEY);
6753
  url.searchParams.delete("codex_ts");
6754
+ SENSITIVE_ANALYTICS_QUERY_KEYS.forEach((key) => url.searchParams.delete(key));
6755
  return url;
6756
  }
6757
 
 
6772
  }
6773
  }
6774
 
6775
+ const POSTHOG_URL_PROPERTY_KEYS = new Set([
6776
+ "$current_url",
6777
+ "$initial_current_url",
6778
+ "$initial_referrer",
6779
+ "$referrer",
6780
+ "client_referrer_url",
6781
+ "first_touch_landing_url",
6782
+ "first_touch_referrer_url",
6783
+ "href",
6784
+ "page_location",
6785
+ ]);
6786
+ const POSTHOG_PATH_PROPERTY_KEYS = new Set([
6787
+ "$pathname",
6788
+ "first_touch_landing_path",
6789
+ "outer_page_path",
6790
+ "page_path",
6791
+ ]);
6792
+
6793
+ function sanitizePostHogAutomaticUrlProperties(event) {
6794
+ const containers = [
6795
+ event?.properties,
6796
+ event?.properties?.$set,
6797
+ event?.properties?.$set_once,
6798
+ ];
6799
+ containers.forEach((container) => {
6800
+ if (!container || typeof container !== "object") {
6801
+ return;
6802
+ }
6803
+ POSTHOG_URL_PROPERTY_KEYS.forEach((key) => {
6804
+ if (typeof container[key] === "string") {
6805
+ container[key] = sanitizedAnalyticsUrl(container[key]);
6806
+ }
6807
+ });
6808
+ POSTHOG_PATH_PROPERTY_KEYS.forEach((key) => {
6809
+ if (typeof container[key] !== "string") {
6810
+ return;
6811
+ }
6812
+ try {
6813
+ const url = stripInternalTrackingParams(
6814
+ new URL(container[key], window.location.origin)
6815
+ );
6816
+ container[key] = `${url.pathname}${url.search}`;
6817
+ } catch {
6818
+ // Preserve malformed values rather than breaking analytics.
6819
+ }
6820
+ });
6821
+ });
6822
+ return event;
6823
+ }
6824
+
6825
  const HFVIEWER_FIRST_TOUCH_ATTRIBUTION_KEY = "hfviewer:firstTouchAttribution:v1";
6826
  const HFVIEWER_ATTRIBUTION_VISIT_ID_KEY = "hfviewer:attributionVisitId:v1";
6827
  const ATTRIBUTION_QUERY_KEYS = [
 
6838
  "ttclid",
6839
  "li_fat_id",
6840
  "mcp_token",
6841
+ "space_journey_id",
6842
+ "space_interaction",
6843
  ];
6844
  let cachedAttributionPayload = null;
6845
 
 
6978
  page_title: document.title,
6979
  page_path: sanitizedAnalyticsPagePath(),
6980
  page_location: sanitizedAnalyticsUrl(),
6981
+ ...HFVIEWER_PRODUCT_CONTEXT,
6982
  ...params,
6983
  ...(tracking
6984
  ? {
 
7273
  hfviewer_hf_name: identity.name || undefined,
7274
  hfviewer_last_identified_source: source || "hf_oauth_session",
7275
  });
7276
+ } else if (identity.hfUsername && !internalMatch && !shouldSuppressPostHogForInternalTracking()) {
7277
+ void loadPostHogClient().then((client) => {
7278
+ try {
7279
+ const distinctId = `hf:${identity.hfUsername}`;
7280
+ const personProperties = {
7281
+ hfviewer_authenticated: true,
7282
+ hfviewer_auth_provider: "hugging_face",
7283
+ hfviewer_hf_username: identity.hfUsername,
7284
+ hfviewer_hf_name: identity.name || undefined,
7285
+ hfviewer_last_identified_source: source || "hf_oauth_session",
7286
+ };
7287
+ client?.identify?.(distinctId, personProperties);
7288
+ latestPostHogIdentity = { email: "", personProperties };
7289
+ } catch {
7290
+ // Analytics identity is best-effort only.
7291
+ }
7292
+ });
7293
  }
7294
  trackPendingHfOauthReturnFromSession(payload, source);
7295
  return internalMatch;
 
7461
  ads_allowed: hasAdsConsent,
7462
  allowed_all: Boolean(consent?.allowed),
7463
  };
7464
+ if (hasAnalyticsConsent) {
7465
+ flushPendingExternalHfAnalyticsIdentity();
7466
+ }
7467
  const categories = consent?.categories || {};
7468
  const categoryEntries = Object.entries(categories)
7469
  .filter(([, value]) => typeof value === "boolean")
 
7607
  }
7608
  } catch {}
7609
  }
7610
+ return sanitizePostHogAutomaticUrlProperties(event);
7611
  },
7612
  disable_session_recording:
7613
  disableMobileSessionRecording || Boolean(internalAutomationLabel),
 
7629
  window.posthog.register?.({
7630
  hfviewer_surface: "top_level_shell",
7631
  is_visualizer_frame: false,
7632
+ ...HFVIEWER_PRODUCT_CONTEXT,
7633
  anonymous_persistence_scope: latestAnalyticsConsentState.analytics_allowed
7634
  ? "durable_consented"
7635
  : "session_tab",
 
7983
  // and property sanitizer as the rest of the first-party shell.
7984
  window.hfviewerTrackPostHogEvent = trackPostHogEvent;
7985
 
7986
+ let pendingExternalHfAnalyticsIdentity = null;
7987
+
7988
+ function identifyExternalHfAnalyticsIdentity(payload, source = "external_hf_oauth") {
7989
+ if (!payload || payload.authenticated === false) {
7990
+ return false;
7991
+ }
7992
+ if (!latestAnalyticsConsentState.analytics_allowed) {
7993
+ pendingExternalHfAnalyticsIdentity = { payload, source };
7994
+ return false;
7995
+ }
7996
+ pendingExternalHfAnalyticsIdentity = null;
7997
+ identifyHfViewerPostHogUserFromSession(payload, source);
7998
+ return true;
7999
+ }
8000
+
8001
+ function flushPendingExternalHfAnalyticsIdentity() {
8002
+ if (!pendingExternalHfAnalyticsIdentity) {
8003
+ return false;
8004
+ }
8005
+ const pending = pendingExternalHfAnalyticsIdentity;
8006
+ return identifyExternalHfAnalyticsIdentity(pending.payload, pending.source);
8007
+ }
8008
+
8009
+ window.hfviewerAnalyticsBridge = Object.freeze({
8010
+ capture: trackPostHogEvent,
8011
+ hasAnalyticsConsent: () => latestAnalyticsConsentState.analytics_allowed === true,
8012
+ identifyHfSession: identifyExternalHfAnalyticsIdentity,
8013
+ productContext: () => ({ ...HFVIEWER_PRODUCT_CONTEXT }),
8014
+ });
8015
+
8016
  function identifyPostHogUserByEmail(email, personProperties = {}) {
8017
  const normalizedEmail = normalizeAnalyticsEmail(email);
8018
  if (!normalizedEmail || shouldSuppressPostHogForInternalTracking()) {
 
58733
  scheduleCommunityShowcasePermalinkScroll();
58734
  });
58735
  syncLandingModelCardButton();
58736
+
index.html CHANGED
@@ -184,7 +184,53 @@
184
  }
185
  }
186
  </script>
187
- <script>window.dataLayer=window.dataLayer||[];window.gtag=function(){};window.VIEWMYMODEL_HF_API_BASE="https://hfviewer.com/api";try{var u=new URL(window.location.href);var m=u.searchParams.get('model');if(m){window.__SX_DEEP_MODEL=m;u.searchParams.delete('model');window.history.replaceState(null,'',u.toString());}}catch(e){}</script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  <style>
189
  /* --- injected by build_space_landing.py: sections stripped for the HF space --- */
190
  header.nav,
@@ -10102,7 +10148,6 @@ body.sx-viewer-open { overflow: hidden; }
10102
  <script>
10103
  (function () {
10104
  var SITE = "https://hfviewer.com";
10105
- var UTM = "utm_source=hf_space&utm_medium=referral&utm_campaign=space-explorer";
10106
  var SITE_SECTIONS = ["models", "glossary", "timeline", "blog", "family", "compare", "model-card-embed", "share", "api", "assets", "visualizer", "next"];
10107
  var MODEL_RE = /^\/([\w.-]+)\/([\w.-]+)\/?$/;
10108
  var overlay = document.getElementById("sx-viewer-overlay");
@@ -10111,17 +10156,64 @@ body.sx-viewer-open { overflow: hidden; }
10111
  var back = document.getElementById("sx-back");
10112
  var openFull = document.getElementById("sx-open");
10113
  var currentPath = null;
 
10114
 
10115
- function siteUrl(path) {
10116
- return SITE + path + (path.indexOf("?") >= 0 ? "&" : "?") + UTM;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10117
  }
10118
 
10119
  function openModel(path) {
10120
  if (currentPath === path) { showOverlay(); return; }
10121
  currentPath = path;
 
10122
  loading.classList.remove("hidden");
10123
- frame.src = siteUrl(path);
10124
- openFull.href = siteUrl(path);
 
 
 
 
 
 
10125
  showOverlay();
10126
  try {
10127
  var u = new URL(window.location.href);
@@ -10140,7 +10232,8 @@ body.sx-viewer-open { overflow: hidden; }
10140
  overlay.classList.remove("open");
10141
  document.body.classList.remove("sx-viewer-open");
10142
  back.style.display = "none";
10143
- openFull.href = SITE + "/?" + UTM;
 
10144
  try {
10145
  var u = new URL(window.location.href);
10146
  u.searchParams.delete("model");
@@ -10148,7 +10241,17 @@ body.sx-viewer-open { overflow: hidden; }
10148
  } catch (e) { /* ignore */ }
10149
  }
10150
 
10151
- frame.addEventListener("load", function () { loading.classList.add("hidden"); });
 
 
 
 
 
 
 
 
 
 
10152
  back.addEventListener("click", closeOverlay);
10153
 
10154
  // window CAPTURE phase: fires before the shell's document-level handlers,
@@ -10165,6 +10268,18 @@ body.sx-viewer-open { overflow: hidden; }
10165
  var isSite = url.origin === SITE || url.origin === window.location.origin;
10166
  if (!isSite) return; // external links (HF, X, ...) behave normally
10167
  var path = url.pathname;
 
 
 
 
 
 
 
 
 
 
 
 
10168
  if (path === "/" || path === "") {
10169
  e.preventDefault();
10170
  e.stopPropagation();
@@ -10182,7 +10297,7 @@ body.sx-viewer-open { overflow: hidden; }
10182
  // genuine site page: open the real site in a new tab, utm-tagged
10183
  e.preventDefault();
10184
  e.stopPropagation();
10185
- window.open(siteUrl(path + url.search.replace(/^\?/, "?") ), "_blank", "noopener");
10186
  }
10187
  }, true);
10188
 
 
184
  }
185
  }
186
  </script>
187
+ <script>
188
+ window.dataLayer = window.dataLayer || [];
189
+ window.gtag = function () {};
190
+ window.VIEWMYMODEL_HF_API_BASE = "https://hfviewer.com/api";
191
+ window.HFVIEWER_PRODUCT_CONTEXT = Object.freeze({
192
+ product_surface: "hf_space",
193
+ space_repo: "embedl/hfviewer",
194
+ space_revision: "hf-space-2e4848474064",
195
+ });
196
+ try {
197
+ var initialUrl = new URL(window.location.href);
198
+ var initialModel = initialUrl.searchParams.get("model");
199
+ if (initialModel) {
200
+ window.__SX_DEEP_MODEL = initialModel;
201
+ initialUrl.searchParams.delete("model");
202
+ window.history.replaceState(null, "", initialUrl.toString());
203
+ }
204
+ var signedAccess = initialUrl.searchParams.get("__sign") || "";
205
+ var signedPayloadPart = signedAccess.split(".")[1] || "";
206
+ if (signedPayloadPart) {
207
+ signedPayloadPart = signedPayloadPart.replace(/-/g, "+").replace(/_/g, "/");
208
+ while (signedPayloadPart.length % 4) signedPayloadPart += "=";
209
+ var signedPayload = JSON.parse(atob(signedPayloadPart));
210
+ var signedUsername = String(signedPayload?.onBehalfOf?.user || "")
211
+ .trim()
212
+ .replace(/^@+/, "")
213
+ .toLowerCase();
214
+ if (signedUsername === "hannesvonessen") {
215
+ window.localStorage?.setItem(
216
+ "hfviewer:internalTracking:v1",
217
+ JSON.stringify({
218
+ tracking_actor: "internal",
219
+ tracking_actor_label: "allowlisted_hf_username:hannesvonessen",
220
+ hfviewer_internal: true,
221
+ hfviewer_staff: true,
222
+ hfviewer_internal_reason: "allowlisted_hf_username",
223
+ hf_username: signedUsername,
224
+ source: "hf_space_signed_access",
225
+ set_at: new Date().toISOString(),
226
+ })
227
+ );
228
+ }
229
+ }
230
+ } catch (error) {
231
+ // Space access hints and cosmetic deep links are best effort only.
232
+ }
233
+ </script>
234
  <style>
235
  /* --- injected by build_space_landing.py: sections stripped for the HF space --- */
236
  header.nav,
 
10148
  <script>
10149
  (function () {
10150
  var SITE = "https://hfviewer.com";
 
10151
  var SITE_SECTIONS = ["models", "glossary", "timeline", "blog", "family", "compare", "model-card-embed", "share", "api", "assets", "visualizer", "next"];
10152
  var MODEL_RE = /^\/([\w.-]+)\/([\w.-]+)\/?$/;
10153
  var overlay = document.getElementById("sx-viewer-overlay");
 
10156
  var back = document.getElementById("sx-back");
10157
  var openFull = document.getElementById("sx-open");
10158
  var currentPath = null;
10159
+ var currentEmbedJourneyId = null;
10160
 
10161
+ function createJourneyId() {
10162
+ try {
10163
+ if (window.crypto && typeof window.crypto.randomUUID === "function") {
10164
+ return window.crypto.randomUUID();
10165
+ }
10166
+ } catch (error) { /* fall through */ }
10167
+ return "space-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10);
10168
+ }
10169
+
10170
+ function trackSpaceEvent(eventName, properties) {
10171
+ try {
10172
+ window.hfviewerAnalyticsBridge?.capture?.(eventName, Object.assign({
10173
+ product_surface: "hf_space",
10174
+ space_repo: "embedl/hfviewer",
10175
+ space_revision: "hf-space-2e4848474064",
10176
+ }, properties || {}));
10177
+ } catch (error) { /* analytics must never break navigation */ }
10178
+ }
10179
+
10180
+ function siteUrl(path, interaction, journeyId) {
10181
+ var url = new URL(path || "/", SITE);
10182
+ url.searchParams.set("utm_source", "hf_space");
10183
+ url.searchParams.set("utm_medium", "referral");
10184
+ url.searchParams.set("utm_campaign", "space-explorer");
10185
+ url.searchParams.set("utm_content", interaction);
10186
+ url.searchParams.set("space_interaction", interaction);
10187
+ url.searchParams.set("space_journey_id", journeyId);
10188
+ return url.toString();
10189
+ }
10190
+
10191
+ function openWebsite(path, source) {
10192
+ var journeyId = createJourneyId();
10193
+ var destination = siteUrl(path, "open_website", journeyId);
10194
+ trackSpaceEvent("space_to_hfviewer_clicked", {
10195
+ space_journey_id: journeyId,
10196
+ space_interaction: "open_website",
10197
+ destination_path: new URL(destination).pathname,
10198
+ source: source || "space_link",
10199
+ embedded_model_path: currentPath || undefined,
10200
+ });
10201
+ window.open(destination, "_blank", "noopener");
10202
  }
10203
 
10204
  function openModel(path) {
10205
  if (currentPath === path) { showOverlay(); return; }
10206
  currentPath = path;
10207
+ currentEmbedJourneyId = createJourneyId();
10208
  loading.classList.remove("hidden");
10209
+ frame.src = siteUrl(path, "embedded_viewer", currentEmbedJourneyId);
10210
+ openFull.href = siteUrl(path, "open_website", currentEmbedJourneyId);
10211
+ trackSpaceEvent("space_model_opened", {
10212
+ space_journey_id: currentEmbedJourneyId,
10213
+ space_interaction: "embedded_viewer",
10214
+ model_repo_id: path.replace(/^\//, ""),
10215
+ destination_path: path,
10216
+ });
10217
  showOverlay();
10218
  try {
10219
  var u = new URL(window.location.href);
 
10232
  overlay.classList.remove("open");
10233
  document.body.classList.remove("sx-viewer-open");
10234
  back.style.display = "none";
10235
+ currentEmbedJourneyId = null;
10236
+ openFull.href = siteUrl("/", "open_website", createJourneyId());
10237
  try {
10238
  var u = new URL(window.location.href);
10239
  u.searchParams.delete("model");
 
10241
  } catch (e) { /* ignore */ }
10242
  }
10243
 
10244
+ frame.addEventListener("load", function () {
10245
+ loading.classList.add("hidden");
10246
+ if (currentPath && currentEmbedJourneyId) {
10247
+ trackSpaceEvent("space_embedded_viewer_loaded", {
10248
+ space_journey_id: currentEmbedJourneyId,
10249
+ space_interaction: "embedded_viewer",
10250
+ model_repo_id: currentPath.replace(/^\//, ""),
10251
+ destination_path: currentPath,
10252
+ });
10253
+ }
10254
+ });
10255
  back.addEventListener("click", closeOverlay);
10256
 
10257
  // window CAPTURE phase: fires before the shell's document-level handlers,
 
10268
  var isSite = url.origin === SITE || url.origin === window.location.origin;
10269
  if (!isSite) return; // external links (HF, X, ...) behave normally
10270
  var path = url.pathname;
10271
+ if (a === openFull) {
10272
+ e.preventDefault();
10273
+ e.stopPropagation();
10274
+ openWebsite(currentPath || "/", "viewer_header");
10275
+ return;
10276
+ }
10277
+ if (a.classList.contains("sx-brand")) {
10278
+ e.preventDefault();
10279
+ e.stopPropagation();
10280
+ openWebsite("/", "space_header_brand");
10281
+ return;
10282
+ }
10283
  if (path === "/" || path === "") {
10284
  e.preventDefault();
10285
  e.stopPropagation();
 
10297
  // genuine site page: open the real site in a new tab, utm-tagged
10298
  e.preventDefault();
10299
  e.stopPropagation();
10300
+ openWebsite(path + url.search, "space_content_link");
10301
  }
10302
  }, true);
10303
 
space-auth.js CHANGED
@@ -40,9 +40,46 @@ function userFromResult(result) {
40
  name: String(raw.name || username).trim(),
41
  picture: String(raw.picture || raw.avatar_url || raw.avatarUrl || "").trim(),
42
  profile: String(raw.profile || `https://huggingface.co/${encodeURIComponent(username)}`).trim(),
 
 
43
  };
44
  }
45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  function keepReleasePills() {
47
  if (!authHost) return [];
48
  return Array.from(authHost.children).filter((element) =>
@@ -76,7 +113,13 @@ function makeLoggedOutControl() {
76
  link.href = loginUrl || "#";
77
  if (!loginUrl) link.setAttribute("aria-disabled", "true");
78
  link.addEventListener("click", (event) => {
79
- if (!loginUrl) event.preventDefault();
 
 
 
 
 
 
80
  });
81
  return link;
82
  }
@@ -107,6 +150,11 @@ function makeLoggedInControl(user) {
107
  profile.rel = "noopener";
108
  profile.textContent = "Hugging Face profile";
109
  profile.setAttribute("role", "menuitem");
 
 
 
 
 
110
 
111
  const logout = document.createElement("button");
112
  logout.type = "button";
@@ -114,6 +162,9 @@ function makeLoggedInControl(user) {
114
  logout.textContent = "Log out";
115
  logout.setAttribute("role", "menuitem");
116
  logout.addEventListener("click", () => {
 
 
 
117
  oauthResult = null;
118
  writeStoredResult(null);
119
  renderAuth();
@@ -161,6 +212,7 @@ function cleanCallbackQuery() {
161
 
162
  async function initialize() {
163
  renderAuth();
 
164
  try {
165
  const callbackResult = await oauthHandleRedirectIfPresent();
166
  if (callbackResult) {
@@ -168,9 +220,22 @@ async function initialize() {
168
  writeStoredResult(callbackResult);
169
  cleanCallbackQuery();
170
  renderAuth();
 
 
 
 
 
 
 
 
171
  }
172
  } catch (error) {
173
  console.warn("Hugging Face Space login callback failed", error);
 
 
 
 
 
174
  }
175
 
176
  try {
@@ -178,6 +243,11 @@ async function initialize() {
178
  loginUrl = await oauthLoginUrl(scopes ? { scopes } : undefined);
179
  } catch (error) {
180
  console.warn("Could not prepare Hugging Face Space login", error);
 
 
 
 
 
181
  }
182
  renderAuth();
183
  }
 
40
  name: String(raw.name || username).trim(),
41
  picture: String(raw.picture || raw.avatar_url || raw.avatarUrl || "").trim(),
42
  profile: String(raw.profile || `https://huggingface.co/${encodeURIComponent(username)}`).trim(),
43
+ subject: String(raw.sub || raw.id || "").trim(),
44
+ email: String(raw.email || "").trim().toLowerCase(),
45
  };
46
  }
47
 
48
+ function trackAuthEvent(eventName, properties = {}) {
49
+ try {
50
+ window.hfviewerAnalyticsBridge?.capture?.(eventName, {
51
+ product_surface: "hf_space",
52
+ space_repo: "embedl/hfviewer",
53
+ space_revision: "hf-space-2e4848474064",
54
+ auth_provider: "hugging_face",
55
+ ...properties,
56
+ });
57
+ } catch {
58
+ // Authentication must remain independent of analytics availability.
59
+ }
60
+ }
61
+
62
+ function identifyAuthenticatedUser(user, source) {
63
+ if (!user?.username) return;
64
+ try {
65
+ window.hfviewerAnalyticsBridge?.identifyHfSession?.(
66
+ {
67
+ authenticated: true,
68
+ email: user.email || "",
69
+ user: {
70
+ id: user.subject || "",
71
+ username: user.username,
72
+ name: user.name || user.username,
73
+ email: user.email || "",
74
+ },
75
+ },
76
+ source || "hf_space_native_oauth"
77
+ );
78
+ } catch {
79
+ // Consent-gated identity is best effort only.
80
+ }
81
+ }
82
+
83
  function keepReleasePills() {
84
  if (!authHost) return [];
85
  return Array.from(authHost.children).filter((element) =>
 
113
  link.href = loginUrl || "#";
114
  if (!loginUrl) link.setAttribute("aria-disabled", "true");
115
  link.addEventListener("click", (event) => {
116
+ if (!loginUrl) {
117
+ event.preventDefault();
118
+ return;
119
+ }
120
+ trackAuthEvent("space_hf_login_clicked", {
121
+ component: "space_nav",
122
+ });
123
  });
124
  return link;
125
  }
 
150
  profile.rel = "noopener";
151
  profile.textContent = "Hugging Face profile";
152
  profile.setAttribute("role", "menuitem");
153
+ profile.addEventListener("click", () => {
154
+ trackAuthEvent("space_hf_profile_opened", {
155
+ component: "space_nav",
156
+ });
157
+ });
158
 
159
  const logout = document.createElement("button");
160
  logout.type = "button";
 
162
  logout.textContent = "Log out";
163
  logout.setAttribute("role", "menuitem");
164
  logout.addEventListener("click", () => {
165
+ trackAuthEvent("space_hf_logout", {
166
+ component: "space_nav",
167
+ });
168
  oauthResult = null;
169
  writeStoredResult(null);
170
  renderAuth();
 
212
 
213
  async function initialize() {
214
  renderAuth();
215
+ identifyAuthenticatedUser(userFromResult(oauthResult), "hf_space_native_oauth_restored");
216
  try {
217
  const callbackResult = await oauthHandleRedirectIfPresent();
218
  if (callbackResult) {
 
220
  writeStoredResult(callbackResult);
221
  cleanCallbackQuery();
222
  renderAuth();
223
+ const user = userFromResult(callbackResult);
224
+ identifyAuthenticatedUser(user, "hf_space_native_oauth_callback");
225
+ trackAuthEvent("space_hf_login_completed", {
226
+ component: "space_nav",
227
+ username_present: !!user?.username,
228
+ subject_present: !!user?.subject,
229
+ email_present: !!user?.email,
230
+ });
231
  }
232
  } catch (error) {
233
  console.warn("Hugging Face Space login callback failed", error);
234
+ trackAuthEvent("space_hf_login_failed", {
235
+ component: "space_nav",
236
+ failure_stage: "oauth_callback",
237
+ error_name: String(error?.name || "Error").slice(0, 80),
238
+ });
239
  }
240
 
241
  try {
 
243
  loginUrl = await oauthLoginUrl(scopes ? { scopes } : undefined);
244
  } catch (error) {
245
  console.warn("Could not prepare Hugging Face Space login", error);
246
+ trackAuthEvent("space_hf_login_failed", {
247
+ component: "space_nav",
248
+ failure_stage: "login_url",
249
+ error_name: String(error?.name || "Error").slice(0, 80),
250
+ });
251
  }
252
  renderAuth();
253
  }