# Infralytiqs Client Bootstrap — Publicis Sapient (`ps`)

Client-specific Infralytiqs bootstrap for the Publicis Sapient family of
Asset Share Commons (ASC) sites — currently
**https://psassets.publicissapient.com** and **https://ps-dev.adobedam.com**.

This project ships a single minified JavaScript file (`dist/infralytiqs-bootstrap.min.js`)
that is deployed to CloudFront and embedded in the host page **after** the
shared Infralytiqs SDK has loaded.

```
Host page <head>
  ├─ <script>window.IL_SERVER_URL = "…"; window.IL_TENANT_ID = "…"; …</script>      ← from Sightly
  ├─ <script src="https://assets.infralytiqs.com/cdn/infralytiqs.min.js"></script>  ← shared SDK (CDN)
  └─ <script src="https://assets.infralytiqs.com/cdn/clients/publicis/ps/infralytiqs-bootstrap.min.js"></script>
```

## Tracked events

### Login page (anonymous)

| Event              | Trigger                                          | Notes |
| ------------------ | ------------------------------------------------ | --- |
| `login_page_view`  | Anonymous user lands on the login page           | Fired in addition to the SDK's auto `page_view` so the Reports login funnel can segment login-page traffic before identify(). |
| `login_attempt`    | Click on **Sign in** or form submit              | Captures the typed username only — **never** the password. Calls `identify(username)` so subsequent events carry it. |
| `sso_click`        | Click on **Login with Lion Login** SSO button    | Sends `method: lion_login`, `sso_provider: lion`. |
| `terms_click`      | Click on **Terms and Conditions** link           | Sends the link `href` and visible text. |

### Asset Share Commons (`search_context = "asc"`, authenticated)

The user is resolved via `/libs/granite/security/currentuser.json` on every
page (cached in `localStorage`) and identified to the SDK so every event
below carries `user_id` in addition to the SDK's `anonymous_id` / `session_id`.

| Event               | Trigger                                                                 | Dimensions                                                                                                                                                                                                                                  |
| ------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `search_executed`   | Any page load whose URL carries `?fulltext=…`                           | `search_term`, `semantic_search`, `fuzzy_search`, `filters_json` (JSON array of `{group, name, property, operation, values[]}` — `name` is the right-rail section heading like `"ASSET TYPE"`, resolved by cross-referencing the URL property against the rail's `cmp-predicate` blocks), `filters_dom_json` (belt-and-braces JSON of `{name, values[]}` produced by sweeping the rail for checked inputs — catches DOM-only state the URL didn't carry), `orderby`, `order_dir`, `layout`, `search_context="asc"`, `page_url`, `page_referrer`, `search_stats_ready` (`"true"` when the search-statistics widget had populated before we fired). Metrics: `filter_count`, `filter_dom_count`, `result_offset`, `result_limit`, **`search_time_ms`** (server-reported search latency read from the right-rail statistics widget), **`result_total`** (total asset count), **`result_pages`** (total page count, when the widget exposes one), **`result_displayed`** (assets shown on the current results page). The bootstrap briefly observes the statistics container with a `MutationObserver` so the timing reflects the search that just executed, not a stale value left over from the previous in-page navigation; if the widget never populates within 3 s the event still fires with `search_stats_ready=false`. The raw label→value tuples (first 240 chars of JSON) are also emitted as `search_stats_raw_json` so any future label change in ASC can be diagnosed from ClickHouse without re-instrumenting the page. |
| `asset_preview`     | Any page load matching `/details/<type>.html/<assetPath>`               | `asset_path` (`/content/dam/…`), `asset_type` (`image`, `video`, `document`, `3d`, …), `asset_name`, `asset_extension`, `page_url`, `page_referrer`. |
| `asset_download`    | Network `POST` to ASC `…/actions/download/.../download.download-asset-renditions.zip` | `asset_path` (from form body `path`), `rendition_name` (from `renditionName`), `download_endpoint`, `page_url`. **Intercepts `fetch` and `XMLHttpRequest`** so it fires whether the user used the card menu, the detail-page button, or a rendition picker. |
| `asset_add_to_cart` | Click on a per-tile / per-card Add-to-Cart button. Detected primarily by `data-asset-share-id="add-to-cart"` (the ASC convention), with `[data-il-cart-add]` / `[data-cmp-cart-action="add"]` and a visible-text `/add to cart/i` regex as fallbacks. The icon-only variant is detected via data-attrs alone (no text required). | `asset_path` — read from `data-asset-share-asset` directly when the button is the ASC variant, or walked up the DOM via `data-asset-path` / `data-asset` / `data-foundation-collection-item-id` / nearest `<a href="/details/…">`. `page_url`. |
| `asset_share`       | Click on an asset-level Share button (`data-asset-share-id="share-asset"`, `[data-il-share]`, `[data-cmp-share-action]`, or visible-text `/share/i`). Explicitly excludes the cart-level Share Cart button so a click on it cannot fall through. | `asset_path`, `page_url`. |
| `cart_open`         | Click anywhere inside the header cart icon link (`a.item > i.cart.icon`, `[data-asset-share-id="cart"]`). | `page_url`. Subtype: `cart_action`. |
| `cart_share_initiated` | Click on the cart-modal "Share Cart" button (`data-asset-share-id="share-all"`). Funnel-only intent — the actual share happens when the user fills the share modal and submits. | `cart_asset_paths_json` (JSON array of `/content/dam/...` paths scraped from the cart modal). Metric: `cart_item_count`. Subtype: `cart_action`. |
| `cart_share`        | Submit of the share-modal form (`form.cmp-modal-share` / `form[class*="cmp-modal-share"]`). Fires the moment the user submits, BEFORE jQuery validation may cancel — so the funnel captures every share attempt. | `share_emails` (comma-joined, deduped, lowercase-key dedupe, 480-char cap), `share_is_public` (`"true"` / `"false"` — taken from the "Share Publicly" checkbox OR inferred from non-empty dates), `share_start_date`, `share_end_date` (`yyyy-mm-dd`, only set when `share_is_public="true"`), `cart_asset_paths_json` (carried over from the cart snapshot captured at button-click time so it survives the cart modal closing). Metrics: `share_emails_count`, `share_date_range_days` (`end - start`, clamped to ≥ 0), `cart_item_count`. Subtype: `cart_action`. |
| `cart_download`     | Click on the cart-modal "Download Cart" button (`data-asset-share-id="download-all"`). The user's confirmed intent to start the cart-download pipeline — the actual binaries arrive asynchronously and emit `asset_download` events when the user clicks the per-row download anchors in the downloads modal. | Same shape as `cart_share`: `cart_asset_paths_json`, `cart_item_count`. Subtype: `cart_action`. |
| `downloads_open`    | Click anywhere inside the header download icon link (`a.item > i.download.icon`, `[data-asset-share-id="downloads"]`). | `page_url`. Subtype: `cart_action`. |
| `asset_download`    | (a) Network `POST` to ASC `…/actions/download/.../download.download-asset-renditions.zip` — `download_method="ajax"`. (b) Click on a per-artifact download anchor in the downloads modal (`a[href*="downloadbinaries.json"]`) — `download_method="binary_link_click"`, with `download_source="downloads_modal"`. | (a) `asset_path` (form body `path`), `rendition_name` (form body `renditionName`), `download_endpoint`, `page_url`. (b) `download_id` and `artifact_id` (parsed off the anchor's query string), `download_endpoint` (URL minus query), `download_source`, `download_method`, `page_url`. |
| `download_removed`  | Click on the per-row "remove from downloads" button (`data-asset-share-id="remove-from-downloads"`). | `download_id` (from `data-asset-share-download-id`), `page_url`. Subtype: `cart_action`. |

### Asset Explorer (`search_context = "asset_explorer"`, authenticated)

`/asset-explorer.html#/content/dam/...` is the React SPA front-end. Search,
folder browsing, and the Search Options modal live entirely in client-side
state, so the bootstrap routes off DOM events rather than URL parsing.

| Event               | Trigger                                                                                                                                                                                                                                       | Dimensions                                                                                                                                                                                                                                                                |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `search_executed`   | Any of: Enter inside the search input, native form submit, Search Options modal close, right-rail filter click, debounced live-typing pause (800 ms). Overlapping triggers on the same user action collapse into one event via `(term, semantic, fuzzy, folder, filters)` dedup. | `search_term`, `semantic_search` / `fuzzy_search` (read from the toggles in the open Search Options modal — falls back through native checkbox → `aria-checked` → `data-state` → class-name heuristic; emits `""` when the row isn't currently rendered), `folder_path` (the `#/content/dam/...` hash), `filters_dom_json` (JSON of `{name, values[]}` blocks scraped from the right-rail facet panel via native-checkbox / ARIA / data-state / class-name heuristics — grouped under the section heading the chip lives under), `search_context="asset_explorer"`, `search_trigger` (which hook fired: `enter` / `submit` / `options_close` / `filter_click` / `input_debounced`), `page_url`, `page_referrer`. Metrics: `filter_dom_count`, same `search_*` stats as ASC populated from the rail's `_statContainer_` (`<N> DISPLAYED <N> TOTAL <N> MILLISECONDS` tiles paired via `_value_` / `_label_` siblings; regex fallback over the container's text if structured markup isn't present). Subtype: `asset_search`. |
| `folder_navigation` | Any of: `hashchange`, click on a folder in the left-side directory tree, expand a tree node, collapse a tree node, initial page load (when a folder is already in the hash route). | `folder_path` (current hash route), `search_context="asset_explorer"`, `tree_action` (`page_load` / `hash_change` / `click` / `expand` / `collapse` — derived from `aria-expanded` mutation when available, otherwise `click`), `tree_target_label` (visible text of the clicked tree node), `tree_target_class_hint` (first 240 chars of the clicked element's className — diagnostic, to refine action detection from data later), `page_url`, `page_referrer`. Subtype: `asset_browse`. No stats wait. A single user gesture can emit one `click`/`expand`/`collapse` event AND a `hash_change` event when the click also navigates — they are intentionally NOT deduped so dashboards can split intent (the user opened a node) from outcome (the route changed). |
| `asset_preview`     | Shared with ASC — fires on a `/details/<type>.html/<assetPath>` navigation (the explorer hands off to ASC for full detail views), and on body-level clicks that resolve to an asset path attribute.                                            | Same as ASC. |
| `asset_download`, `asset_add_to_cart`, `asset_share` | Shared with ASC — same network interception and click delegation.                                                                                                                                                          | Same as ASC. |

Every event also picks up the global enrichment dimensions (browser, viewport,
screen, timezone, language, color scheme, tenant, site, origin) plus the
SDK's transport-level fields (timestamps, anonymous_id, session_id, user_id,
UTM, country) so the Reports UI can slice by them without per-event
instrumentation.

### How each event is wired

Three strategies, picked to be as DOM-independent as practical:

1. **URL-driven** (`search_executed`, `asset_preview`, `login_page_view`) —
   the host page navigates via real GET requests, so the resulting URL is
   the authoritative record of what the user did. The bootstrap parses
   `location.pathname` / `location.search` on every page load and emits
   the event. This survives bookmarks, browser-back, and shared links.
2. **Network-driven** (`asset_download`) — `window.fetch` and
   `XMLHttpRequest.prototype.{open,send}` are wrapped once per page. The
   wrapper matches the ASC download endpoint regex and reads the asset
   path + rendition name straight from the form-encoded POST body. No
   DOM dependency at all, so it fires whether the user used the card
   menu, the detail-page button, or a rendition-picker dropdown.
3. **Click delegation** (`asset_add_to_cart`, `asset_share`) — a single
   body-level capturing listener uses `closest()` to find a matching
   button regardless of which descendant icon/label the user actually
   clicked. Survives SPA re-renders without needing a `MutationObserver`.

## Configuration contract

The bootstrap reads its configuration from `window.IL_*` globals that **must
be set by the host page before this script runs** (the AEM Sightly fragment
`Infralytiqs.html` injects them server-side):

| Global              | Required | Description                              |
| ------------------- | -------- | ---------------------------------------- |
| `IL_SERVER_URL`     | Yes      | Base URL of the st-ck-server (Infralytiqs API). |
| `IL_TENANT_ID`      | Yes      | Tenant id assigned in the Infralytiqs settings. |
| `IL_SITE_ID`        | Yes      | Site id assigned in the Infralytiqs settings.   |
| `IL_DB_NAME`        | No       | ClickHouse DB name for the tenant/site.  |
| `IL_DEBUG`          | No       | `true` enables SDK console logging.      |
| `IL_CAPTURE_LOCATION` | No     | `true` opts the SDK into the browser geolocation prompt. |
| `IL_PS_SELECTORS`   | No       | Object overriding the default selectors (see below). |

### Selector overrides

The bootstrap matches DOM controls with a robust default set (id, name,
data-attribute, href, and visible-text). To opt out of the heuristic, set
`window.IL_PS_SELECTORS` before this script loads:

```html
<script>
  window.IL_PS_SELECTORS = {
    // Login page
    loginForm:    '#loginForm',
    signInButton: '#signInButton',
    userField:    'input[name="username"]',
    ssoButton:    '#lionLoginBtn',
    termsLink:    'a#termsAndConditions',
    // Asset Share Commons
    searchInput:      'input[name="fulltext"]',
    filterRail:       '#rail',
    searchStatistics: '.cmp.cmp-search-statistics',
    addToCart:        '[data-il-cart-add]',
    shareButton:      '[data-il-share]',
    // Asset Explorer (React)
    assetExplorerRoot:            '[class*="_directoryExplorer_"]',
    assetExplorerForm:            '[class*="_directoryExplorer_"] form',
    assetExplorerSearchInput:     '[class*="_directoryExplorer_"] input[type="text"]',
    assetExplorerSearchOptionRow: '[class*="_searchOptionRow_"]',
    assetExplorerFilterRail:      '[class*="_filterWrapper_"]',
    assetExplorerFilterSection:   '[class*="_filterSection_"], [class*="_facet_"]',
    assetExplorerSearchStatistics:'[class*="_statContainer_"]',
    assetExplorerFolderTree:      '[class*="_directoryTreeContent_"]'
  };
</script>
```

The easiest pattern for ASC components is to add an opt-in
`data-il-cart-add` / `data-il-share` attribute (or `data-asset-path`
on the parent card) so the bootstrap stops relying on the visible-text
fallback altogether.

### User-identity override

By default the bootstrap resolves the logged-in user by:

1. Calling `window.IL_PS_USER_RESOLVER()` if you defined it (sync string
   or a `Promise<string>`).
2. Reading `<meta name="cq:user">` (or `name="user"` / `name="user-id"`).
3. Falling back to `GET /libs/granite/security/currentuser.json` and
   using its `authorizableId` / `id`.

To bypass all three, supply a resolver:

```html
<script>
  window.IL_PS_USER_RESOLVER = function () {
    return window.MY_AUTH_PROVIDER.getUsername();
  };
</script>
```

## Building

```bash
npm install
npm run build       # produces dist/infralytiqs-bootstrap.{js,min.js}
npm run dev         # watch mode
```

The `dist/` directory contains both an unminified (`infralytiqs-bootstrap.js`)
and a minified (`infralytiqs-bootstrap.min.js`) IIFE. Only the minified file
is deployed to CloudFront.

## Deployment

Deployment of `dist/infralytiqs-bootstrap.min.js` to the CloudFront-fronted
S3 bucket is handled by a Jenkins pipeline (TBD). The destination URL is
expected to be something along the lines of:

```
https://assets.infralytiqs.com/cdn/clients/publicis/ps/infralytiqs-bootstrap.min.js
```
