You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@flagon.apache.org by au...@apache.org on 2026/10/11 15:43:45 UTC

(flagon) 01/01: docs(userale): add OpenTelemetry + Grafana/Loki/Tempo demo stack and auth/OTLP documentation (#141, #134, #101, #102)

This is an automated email from the ASF dual-hosted git repository.

brucearctor pushed a commit to branch feat/otel-demo-and-docs
in repository https://gitbox.apache.org/repos/asf/flagon.git

commit 6285aceda0b01516419197033621e1fa75855db1
Author: Bruce Arctor <50...@users.noreply.github.com>
AuthorDate: Sun Oct 11 08:42:41 2026 -0700

    docs(userale): add OpenTelemetry + Grafana/Loki/Tempo demo stack and auth/OTLP documentation (#141, #134, #101, #102)
---
 products/userale/example/otel-demo/README.md       |  81 ++++++
 .../userale/example/otel-demo/docker-compose.yml   |  60 +++++
 .../grafana/dashboards/userale-overview.json       | 146 +++++++++++
 .../provisioning/dashboards/dashboards.yaml        |  28 +++
 .../provisioning/datasources/datasources.yaml      |  50 ++++
 products/userale/example/otel-demo/index.html      | 216 ++++++++++++++++
 .../userale/example/otel-demo/loki-config.yaml     |  62 +++++
 .../example/otel-demo/otel-collector-config.yaml   |  78 ++++++
 .../userale/example/otel-demo/tempo-config.yaml    |  43 ++++
 products/userale/example/server/server.js          |  36 ++-
 products/userale/packages/flagon-userale/README.md | 280 ++++++++++++---------
 11 files changed, 946 insertions(+), 134 deletions(-)

diff --git a/products/userale/example/otel-demo/README.md b/products/userale/example/otel-demo/README.md
new file mode 100644
index 0000000..1901b7b
--- /dev/null
+++ b/products/userale/example/otel-demo/README.md
@@ -0,0 +1,81 @@
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+-->
+
+# Apache Flagon UserALE.js — OpenTelemetry + Grafana/Loki/Tempo Demo
+
+This directory provides a zero-build, full-stack observability environment that ingests behavioral telemetry from **Apache Flagon UserALE.js** via OpenTelemetry (`OTLP/JSON`) into the **OpenTelemetry Collector**, stores logs in **Grafana Loki** and traces in **Grafana Tempo**, and visualizes both in **Grafana**.
+
+## Architecture
+
+```
+Browser (UserALE.js OtlpJsonExporter)
+   │  POST http://localhost:4318/v1/logs (OTLP/JSON + W3C traceId/spanId)
+   ▼
+OpenTelemetry Collector (otelcol-contrib:0.114.0)
+   ├──► Grafana Loki (:3100/otlp)  ──┐
+   └──► Grafana Tempo (:4317)      ──┼──► Grafana (:3000)
+                                          "Apache Flagon UserALE — Behavioral Telemetry & Trace Correlation"
+```
+
+## 5-Minute Quickstart
+
+1. **Build UserALE.js** (from `products/userale`):
+
+   ```bash
+   pnpm install
+   pnpm run build
+   ```
+
+2. **Start the OpenTelemetry Collector, Loki, Tempo, and Grafana stack**:
+
+   ```bash
+   docker compose -f products/userale/example/otel-demo/docker-compose.yml up -d
+   ```
+
+3. **Open the Demo Page & Grafana**:
+   - Open `products/userale/example/otel-demo/index.html` in your browser (or serve `products/userale` via any static server).
+   - Click buttons, type in the search input, and click **"Simulate Traced Checkout API Call"** to emit a UserALE behavioral log correlated with a W3C `traceId` and `spanId`.
+   - Open **Grafana** at [http://localhost:3000/d/flagon-userale-otel](http://localhost:3000/d/flagon-userale-otel) (no login required).
+   - Expand any log row with a `traceId` in the **Live UserALE OTLP Behavioral Stream** panel and click the **Tempo** link next to `TraceID` to jump directly from the user's DOM interaction to the backend distributed trace!
+
+4. **Tear Down**:
+   ```bash
+   docker compose -f products/userale/example/otel-demo/docker-compose.yml down
+   ```
+
+## CORS Configuration on the OpenTelemetry Collector
+
+Because UserALE.js ships telemetry directly from the browser (`Content-Type: application/json`, plus optional `Authorization` or `x-api-key` headers), your OpenTelemetry Collector `otlp` HTTP receiver must enable `cors`:
+
+```yaml
+receivers:
+  otlp:
+    protocols:
+      http:
+        endpoint: 0.0.0.0:4318
+        cors:
+          allowed_origins:
+            - "https://your-app.example.com"
+          allowed_headers:
+            - "Content-Type"
+            - "Authorization"
+            - "x-api-key"
+            - "traceparent"
+            - "tracestate"
+```
diff --git a/products/userale/example/otel-demo/docker-compose.yml b/products/userale/example/otel-demo/docker-compose.yml
new file mode 100644
index 0000000..2571445
--- /dev/null
+++ b/products/userale/example/otel-demo/docker-compose.yml
@@ -0,0 +1,60 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+services:
+  otel-collector:
+    image: otel/opentelemetry-collector-contrib:0.114.0
+    command: ["--config=/etc/otel-collector-config.yaml"]
+    volumes:
+      - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml:ro
+    ports:
+      - "4317:4317" # OTLP gRPC
+      - "4318:4318" # OTLP HTTP (/v1/logs, /v1/traces)
+    depends_on:
+      - loki
+      - tempo
+
+  loki:
+    image: grafana/loki:3.2.0
+    command: ["-config.file=/etc/loki/local-config.yaml"]
+    volumes:
+      - ./loki-config.yaml:/etc/loki/local-config.yaml:ro
+    ports:
+      - "3100:3100"
+
+  tempo:
+    image: grafana/tempo:2.6.0
+    command: ["-config.file=/etc/tempo.yaml"]
+    volumes:
+      - ./tempo-config.yaml:/etc/tempo.yaml:ro
+    ports:
+      - "3200:3200"
+
+  grafana:
+    image: grafana/grafana:11.3.0
+    environment:
+      - GF_AUTH_ANONYMOUS_ENABLED=true
+      - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
+      - GF_AUTH_DISABLE_LOGIN_FORM=true
+    volumes:
+      - ./grafana/provisioning:/etc/grafana/provisioning:ro
+      - ./grafana/dashboards:/var/lib/grafana/dashboards:ro
+    ports:
+      - "3000:3000"
+    depends_on:
+      - loki
+      - tempo
diff --git a/products/userale/example/otel-demo/grafana/dashboards/userale-overview.json b/products/userale/example/otel-demo/grafana/dashboards/userale-overview.json
new file mode 100644
index 0000000..3b17209
--- /dev/null
+++ b/products/userale/example/otel-demo/grafana/dashboards/userale-overview.json
@@ -0,0 +1,146 @@
+{
+  "annotations": {
+    "list": []
+  },
+  "editable": true,
+  "fiscalYearStartMonth": 0,
+  "graphTooltip": 0,
+  "id": null,
+  "links": [],
+  "panels": [
+    {
+      "datasource": {
+        "type": "loki",
+        "uid": "loki"
+      },
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 0
+      },
+      "id": 1,
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom",
+          "showLegend": true
+        },
+        "tooltip": {
+          "mode": "multi",
+          "sort": "desc"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "loki",
+            "uid": "loki"
+          },
+          "expr": "sum by (flagon_event_type) (count_over_time({service_name=~\".+\"} [$__auto]))",
+          "refId": "A"
+        }
+      ],
+      "title": "UserALE Interactions by Event Type",
+      "type": "timeseries"
+    },
+    {
+      "datasource": {
+        "type": "loki",
+        "uid": "loki"
+      },
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 0
+      },
+      "id": 2,
+      "options": {
+        "displayLabels": ["name", "percent"],
+        "legend": {
+          "displayMode": "table",
+          "placement": "right",
+          "showLegend": true
+        },
+        "pieType": "donut"
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "loki",
+            "uid": "loki"
+          },
+          "expr": "topk(10, sum by (flagon_target) (count_over_time({service_name=~\".+\"} [$__range])))",
+          "refId": "A"
+        }
+      ],
+      "title": "Top 10 Interacted DOM Targets",
+      "type": "piechart"
+    },
+    {
+      "datasource": {
+        "type": "loki",
+        "uid": "loki"
+      },
+      "gridPos": {
+        "h": 14,
+        "w": 24,
+        "x": 0,
+        "y": 8
+      },
+      "id": 3,
+      "options": {
+        "dedupStrategy": "none",
+        "enableLogDetails": true,
+        "prettifyLogMessage": true,
+        "showCommonLabels": false,
+        "showLabels": true,
+        "showTime": true,
+        "sortOrder": "Descending",
+        "wrapLogMessage": true
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "loki",
+            "uid": "loki"
+          },
+          "expr": "{service_name=~\".+\"}",
+          "refId": "A"
+        }
+      ],
+      "title": "Live UserALE OTLP Behavioral Stream (Click TraceID to jump to Tempo)",
+      "type": "logs"
+    }
+  ],
+  "refresh": "5s",
+  "schemaVersion": 39,
+  "tags": ["apache-flagon", "userale", "opentelemetry"],
+  "templating": {
+    "list": []
+  },
+  "time": {
+    "from": "now-15m",
+    "to": "now"
+  },
+  "title": "Apache Flagon UserALE — Behavioral Telemetry & Trace Correlation",
+  "uid": "flagon-userale-otel",
+  "version": 1
+}
diff --git a/products/userale/example/otel-demo/grafana/provisioning/dashboards/dashboards.yaml b/products/userale/example/otel-demo/grafana/provisioning/dashboards/dashboards.yaml
new file mode 100644
index 0000000..1b5582a
--- /dev/null
+++ b/products/userale/example/otel-demo/grafana/provisioning/dashboards/dashboards.yaml
@@ -0,0 +1,28 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+apiVersion: 1
+
+providers:
+  - name: "Flagon UserALE Dashboards"
+    orgId: 1
+    folder: "Apache Flagon"
+    type: file
+    disableDeletion: false
+    editable: true
+    options:
+      path: /var/lib/grafana/dashboards
diff --git a/products/userale/example/otel-demo/grafana/provisioning/datasources/datasources.yaml b/products/userale/example/otel-demo/grafana/provisioning/datasources/datasources.yaml
new file mode 100644
index 0000000..cbeb1f4
--- /dev/null
+++ b/products/userale/example/otel-demo/grafana/provisioning/datasources/datasources.yaml
@@ -0,0 +1,50 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+apiVersion: 1
+
+datasources:
+  - name: Loki
+    uid: loki
+    type: loki
+    access: proxy
+    url: http://loki:3100
+    isDefault: true
+    jsonData:
+      derivedFields:
+        - datasourceUid: tempo
+          matcherRegex: '"traceId":"([0-9a-f]{32})"'
+          name: TraceID
+          url: "$${__value.raw}"
+        - datasourceUid: tempo
+          matcherType: label
+          matcherRegex: "trace_id"
+          name: TraceID
+          url: "$${__value.raw}"
+
+  - name: Tempo
+    uid: tempo
+    type: tempo
+    access: proxy
+    url: http://tempo:3200
+    jsonData:
+      tracesToLogsV2:
+        datasourceUid: loki
+        spanStartTimeShift: "-5s"
+        spanEndTimeShift: "5s"
+        filterByTraceID: true
+        filterBySpanID: false
diff --git a/products/userale/example/otel-demo/index.html b/products/userale/example/otel-demo/index.html
new file mode 100644
index 0000000..9b9c5c5
--- /dev/null
+++ b/products/userale/example/otel-demo/index.html
@@ -0,0 +1,216 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+-->
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <title>Apache Flagon UserALE + OpenTelemetry Demo</title>
+    <script
+      src="../../packages/flagon-userale/build/main.global.js"
+      data-otlp-endpoint="http://localhost:4318"
+      data-exporter="otlp"
+      data-tool="flagon-otel-demo"
+      data-version="2.5.0"
+      data-user="demo-user"
+      data-threshold="1"
+      data-interval="1000"
+      data-log-details="true"
+    ></script>
+    <style>
+      body {
+        font-family:
+          system-ui,
+          -apple-system,
+          sans-serif;
+        max-width: 780px;
+        margin: 2rem auto;
+        padding: 0 1rem;
+        line-height: 1.5;
+      }
+      .card {
+        border: 1px solid #d0d7de;
+        border-radius: 8px;
+        padding: 1.25rem;
+        margin-bottom: 1.25rem;
+      }
+      button {
+        padding: 0.6rem 1rem;
+        border-radius: 6px;
+        border: 1px solid #1f883d;
+        background: #1f883d;
+        color: #fff;
+        font-weight: 600;
+        cursor: pointer;
+        margin-right: 0.5rem;
+      }
+      button.secondary {
+        background: #0969da;
+        border-color: #0969da;
+      }
+      pre {
+        background: #f6f8fa;
+        padding: 0.75rem;
+        border-radius: 6px;
+        overflow-x: auto;
+      }
+    </style>
+  </head>
+  <body>
+    <h1>Apache Flagon UserALE.js — OpenTelemetry (OTLP/JSON) Demo</h1>
+    <p>
+      Every interaction on this page is exported directly to the OpenTelemetry
+      Collector at <code>http://localhost:4318/v1/logs</code> and correlated
+      with distributed traces in Grafana Loki &amp; Tempo (<a
+        href="http://localhost:3000"
+        target="_blank"
+        rel="noreferrer"
+        >Open Grafana Dashboard</a
+      >).
+    </p>
+
+    <div class="card">
+      <h2>1. Standard DOM Interactions</h2>
+      <button id="add-to-cart-btn" type="button">Add Item to Cart</button>
+      <button id="Apply-Coupon-Btn" class="secondary" type="button">
+        Apply Discount Code
+      </button>
+      <p>
+        <label for="search-input">Search catalog: </label>
+        <input
+          id="search-input"
+          type="text"
+          placeholder="Type to generate input logs..."
+        />
+      </p>
+    </div>
+
+    <div class="card">
+      <h2>2. W3C Trace Context Correlation (Click-to-Trace)</h2>
+      <p>
+        Click below to generate a W3C <code>traceId</code> &amp;
+        <code>spanId</code>, emit a simulated checkout span to
+        <code>http://localhost:4318/v1/traces</code>, and attach the active
+        <code>traceId</code> to the UserALE behavioral log.
+      </p>
+      <button id="traced-checkout-btn" type="button">
+        Simulate Traced Checkout API Call
+      </button>
+      <pre id="trace-status">No traced action triggered yet.</pre>
+    </div>
+
+    <script>
+      function randomHex(len) {
+        const bytes = new Uint8Array(len / 2);
+        window.crypto.getRandomValues(bytes);
+        return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(
+          "",
+        );
+      }
+
+      let activeTrace = null;
+      if (window.userale) {
+        window.userale.options({
+          traceContext: () => activeTrace,
+        });
+      }
+
+      document
+        .getElementById("traced-checkout-btn")
+        .addEventListener("click", async () => {
+          const traceId = randomHex(32);
+          const spanId = randomHex(16);
+          activeTrace = { traceId, spanId };
+
+          const nowNano = (BigInt(Date.now()) * BigInt(1000000)).toString(10);
+          const endNano = (BigInt(Date.now() + 45) * BigInt(1000000)).toString(
+            10,
+          );
+
+          // Emit a custom UserALE event correlated with the active W3C traceId/spanId
+          if (window.userale) {
+            window.userale.packageCustomLog(
+              {
+                type: "checkout",
+                traceId,
+                spanId,
+              },
+              () => ({ cartTotal: 149.99, currency: "USD" }),
+              true,
+            );
+          }
+
+          // Emit a matching OTLP trace span to the OpenTelemetry Collector (/v1/traces)
+          const tracePayload = {
+            resourceSpans: [
+              {
+                resource: {
+                  attributes: [
+                    {
+                      key: "service.name",
+                      value: { stringValue: "flagon-otel-demo" },
+                    },
+                  ],
+                },
+                scopeSpans: [
+                  {
+                    scope: { name: "checkout-service", version: "1.0.0" },
+                    spans: [
+                      {
+                        traceId,
+                        spanId,
+                        name: "POST /api/checkout",
+                        kind: 3,
+                        startTimeUnixNano: nowNano,
+                        endTimeUnixNano: endNano,
+                        attributes: [
+                          {
+                            key: "http.method",
+                            value: { stringValue: "POST" },
+                          },
+                          {
+                            key: "http.route",
+                            value: { stringValue: "/api/checkout" },
+                          },
+                        ],
+                      },
+                    ],
+                  },
+                ],
+              },
+            ],
+          };
+
+          try {
+            await fetch("http://localhost:4318/v1/traces", {
+              method: "POST",
+              headers: { "Content-Type": "application/json" },
+              body: JSON.stringify(tracePayload),
+            });
+            document.getElementById("trace-status").textContent =
+              `Sent correlated UserALE log + OTLP span!\ntraceId: ${traceId}\nspanId:  ${spanId}`;
+          } catch (err) {
+            document.getElementById("trace-status").textContent =
+              `Generated traceId: ${traceId} (start docker compose to send to collector: ${err.message})`;
+          } finally {
+            setTimeout(() => {
+              activeTrace = null;
+            }, 250);
+          }
+        });
+    </script>
+  </body>
+</html>
diff --git a/products/userale/example/otel-demo/loki-config.yaml b/products/userale/example/otel-demo/loki-config.yaml
new file mode 100644
index 0000000..266a55d
--- /dev/null
+++ b/products/userale/example/otel-demo/loki-config.yaml
@@ -0,0 +1,62 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+auth_enabled: false
+
+server:
+  http_listen_port: 3100
+
+common:
+  instance_addr: 127.0.0.1
+  path_prefix: /tmp/loki
+  storage:
+    filesystem:
+      chunks_directory: /tmp/loki/chunks
+      rules_directory: /tmp/loki/rules
+  replication_factor: 1
+  ring:
+    kvstore:
+      store: inmemory
+
+schema_config:
+  configs:
+    - from: 2024-01-01
+      store: tsdb
+      object_store: filesystem
+      schema: v13
+      index:
+        prefix: index_
+        period: 24h
+
+limits_config:
+  allow_structured_metadata: true
+  otlp_config:
+    resource_attributes:
+      attributes_config:
+        - action: index_label
+          attributes:
+            - service.name
+            - session.id
+    log_attributes:
+      - action: structured_metadata
+        attributes:
+          - event.name
+          - flagon.log_type
+          - flagon.event_type
+          - flagon.target
+          - url.full
+          - user.id
diff --git a/products/userale/example/otel-demo/otel-collector-config.yaml b/products/userale/example/otel-demo/otel-collector-config.yaml
new file mode 100644
index 0000000..05b203d
--- /dev/null
+++ b/products/userale/example/otel-demo/otel-collector-config.yaml
@@ -0,0 +1,78 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+receivers:
+  otlp:
+    protocols:
+      grpc:
+        endpoint: 0.0.0.0:4317
+      http:
+        endpoint: 0.0.0.0:4318
+        # Browser CORS configuration required when shipping logs directly from UserALE.js
+        cors:
+          allowed_origins:
+            - "*"
+          allowed_headers:
+            - "Content-Type"
+            - "Authorization"
+            - "x-api-key"
+            - "traceparent"
+            - "tracestate"
+          max_age: 7200
+
+processors:
+  batch:
+    timeout: 1s
+    send_batch_size: 512
+
+  # Optional OpenTelemetry Transformation Language (OTTL) processor showing how
+  # stock otelcol-contrib can enrich or normalize Flagon UserALE OTLP logs
+  # without requiring a custom Go receiver plugin.
+  transform/flagon:
+    error_mode: ignore
+    log_statements:
+      - context: log
+        statements:
+          - set(attributes["loki.attribute.labels"], "event.name, flagon.log_type, flagon.event_type, flagon.target, user.id")
+      - context: resource
+        statements:
+          - set(attributes["loki.resource.labels"], "service.name, session.id")
+
+exporters:
+  otlphttp/loki:
+    endpoint: http://loki:3100/otlp
+    tls:
+      insecure: true
+
+  otlp/tempo:
+    endpoint: tempo:4317
+    tls:
+      insecure: true
+
+  debug:
+    verbosity: normal
+
+service:
+  pipelines:
+    logs:
+      receivers: [otlp]
+      processors: [transform/flagon, batch]
+      exporters: [otlphttp/loki, debug]
+    traces:
+      receivers: [otlp]
+      processors: [batch]
+      exporters: [otlp/tempo, debug]
diff --git a/products/userale/example/otel-demo/tempo-config.yaml b/products/userale/example/otel-demo/tempo-config.yaml
new file mode 100644
index 0000000..f141510
--- /dev/null
+++ b/products/userale/example/otel-demo/tempo-config.yaml
@@ -0,0 +1,43 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+server:
+  http_listen_port: 3200
+
+distributor:
+  receivers:
+    otlp:
+      protocols:
+        grpc:
+          endpoint: 0.0.0.0:4317
+        http:
+          endpoint: 0.0.0.0:4318
+
+ingester:
+  max_block_duration: 5m
+
+compactor:
+  compaction:
+    block_retention: 1h
+
+storage:
+  trace:
+    backend: local
+    wal:
+      path: /tmp/tempo/wal
+    local:
+      path: /tmp/tempo/blocks
diff --git a/products/userale/example/server/server.js b/products/userale/example/server/server.js
index 7ab21d6..d606ca9 100644
--- a/products/userale/example/server/server.js
+++ b/products/userale/example/server/server.js
@@ -62,7 +62,8 @@ app.use((req, res, next) => {
   res.set({
     "Access-Control-Allow-Origin": "*",
     "Access-Control-Allow-Methods": "GET,PUT,POST,DELETE,OPTIONS",
-    "Access-Control-Allow-Headers": "Content-Type, Authorization, Content-Length, X-Requested-With",
+    "Access-Control-Allow-Headers":
+      "Content-Type, Authorization, Content-Length, X-Requested-With, x-api-key, traceparent, tracestate",
   });
 
   if (req.method === "OPTIONS") return res.sendStatus(200);
@@ -73,25 +74,40 @@ app.use(bodyParser.urlencoded({ extended: true, limit: "100mb" }));
 app.use(bodyParser.json({ limit: "100mb" }));
 app.use(bodyParser.text());
 
-app.use("/build", express.static(path.join(__dirname, "../../packages/flagon-userale/build"))); 
+app.use(
+  "/build",
+  express.static(path.join(__dirname, "../../packages/flagon-userale/build")),
+);
 app.use("/", express.static(__dirname));
 
 app.get("/", (req, res) => res.sendFile("index.html", { root: __dirname }));
-app.get("/ws", (req, res) => res.sendFile("ws-index.html", { root: __dirname }));
-app.get("/no-logging", (req, res) => res.sendFile("no-logging.html", { root: __dirname }));
-
-app.post("/", (req, res) => {
+app.get("/ws", (req, res) =>
+  res.sendFile("ws-index.html", { root: __dirname }),
+);
+app.get("/no-logging", (req, res) =>
+  res.sendFile("no-logging.html", { root: __dirname }),
+);
+
+app.post(["/", "/v1/logs"], (req, res) => {
   const body = typeof req.body === "string" ? JSON.parse(req.body) : req.body;
 
   if (
     (Array.isArray(body) && body.length === 0) ||
-    (typeof body === "object" && body !== null && Object.keys(body).length === 0)
+    (typeof body === "object" &&
+      body !== null &&
+      Object.keys(body).length === 0)
   ) {
     return res.sendStatus(200);
   }
 
   console.log("HTTP POST:", body);
-  writeLogs(body);
+  if (Array.isArray(body)) {
+    writeLogs(body);
+  } else if (body && Array.isArray(body.logs)) {
+    writeLogs(body.logs);
+  } else if (body && Array.isArray(body.resourceLogs)) {
+    writeLogs([body]);
+  }
   res.sendStatus(200);
 });
 
@@ -124,7 +140,9 @@ wss.on("connection", (socket) => {
 
     if (
       (Array.isArray(body) && body.length === 0) ||
-      (typeof body === "object" && body !== null && Object.keys(body).length === 0)
+      (typeof body === "object" &&
+        body !== null &&
+        Object.keys(body).length === 0)
     ) {
       return;
     }
diff --git a/products/userale/packages/flagon-userale/README.md b/products/userale/packages/flagon-userale/README.md
index 845b7bf..4e0b650 100644
--- a/products/userale/packages/flagon-userale/README.md
+++ b/products/userale/packages/flagon-userale/README.md
@@ -41,6 +41,7 @@ Additional documentation and a demonstration can be found at the [Apache Flagon
 1. Visualize and analyze your logs. See our sample [kibana dahsboards](https://github.com/apache/flagon/tree/master/examples/elk/docker) for behavioral analytics.
 
 ## Table of Contents
+
 [What's New](#whats-new-in-version-210)
 [Installation](#installation)
 [Configure](#configure)
@@ -65,23 +66,17 @@ See our [CHANGELOG](https://github.com/apache/flagon/blob/master/products/useral
 Either through cloning our [source repo](https://github.com/apache/flagon) or by using npm:
 
 ```html
-npm install flagon-userale
-
-or
-
-npm install flagon-userale --engine-strict (enforces supported Node.js versions)
-
+npm install flagon-userale or npm install flagon-userale --engine-strict
+(enforces supported Node.js versions)
 ```
 
 To include UserALE as an object in your project, include as a `module`:
 
 ```html
-import * as userale from 'flagon-userale';
-
-or
-
-const userale = require('flagon-userale');
+import * as userale from 'flagon-userale'; or const userale =
+require('flagon-userale');
 ```
+
 Our [webpack example](https://github.com/apache/flagon/tree/master/products/userale/packages/flagon-userale/example/webpackUserAleExample) illustrates this use-case.
 
 You can also include UserALE as a `script-tag`. A pre-built version of the userale script is included in our package and
@@ -90,6 +85,7 @@ repositories:
 ```html
 <script src="./node_modules/flagon-userale/build/userale-2.4.0.min.js"></script>
 ```
+
 Our [script tag example](https://github.com/apache/flagon/tree/master/products/userale/packages/flagon-userale/example) illustrates this use-case
 
 If you include UserALE as a `script-tag`, consider installing via npm as a development dependency:
@@ -121,74 +117,132 @@ Configuration details follow:
 
 If you have included UserALE in your project as a `module`, you will need to use our `userale.options()` function, which exposes library configuration options through our API.
 
-For example, if you do *not* want UserALE to start logging automatically, you can modify this behavior through the `userale.options()` API (`autostart` config).
+For example, if you do _not_ want UserALE to start logging automatically, you can modify this behavior through the `userale.options()` API (`autostart` config).
 
 Then, you can use the `userale.start()` API export to begin logging at the appropriate time during page load or triggered from an event:
 
 ```html
-const changeMe = "me";
-userale.options({
-    "userId": changeMe,
-    "autostart": false,
-    "url": "http://localhost:8000/",
-    "version": "next",
-    "logDetails": false,
-    "sessionId": "this one"
-});
-
-userale.start();
-
+const changeMe = "me"; userale.options({ "userId": changeMe, "autostart": false,
+"url": "http://localhost:8000/", "version": "next", "logDetails": false,
+"sessionId": "this one" }); userale.start();
 ```
 
 Additional examples of `userale.options()` can be found in our [example directory](https://github.com/apache/flagon/tree/master/products/userale/packages/flagon-userale/example).
 
 The complete list of configurable parameters that can be configured via `userale.options()` is:
 
-| Param | Description | Default |
-|---|---|---|
-| url | Logging URL | http://localhost:8000 |
-| autostart | Should UserALE start on page load | true |
-| transmitInterval | Delay between transmit checks | 5000 (ms) |
-| logCountThreshold | Minimum number of logs to send | 5 |
-| userId | User identifier | null |
-| sessionId | Session identifier | null |
-| version | Application version identifier | null |
-| logDetails | Toggle detailed logs (keys pressed and input/change values) | false |
-| resolution | Delay between instances of high frequency logs (mouseover, scroll, etc.) | 500 (ms) |
-| userFromParams | Query param in the page URL to fetch userId from | null |
-| toolName | Name of tool being logged | null |
-| authHeader | Authorization header to be passed to logging endpoint | null |
+| Param             | Description                                                                             | Default               |
+| ----------------- | --------------------------------------------------------------------------------------- | --------------------- |
+| url               | Logging URL (`http://`, `https://`, `ws://`, or `wss://`)                               | http://localhost:8000 |
+| autostart         | Should UserALE start on page load                                                       | true                  |
+| transmitInterval  | Delay between transmit checks                                                           | 5000 (ms)             |
+| logCountThreshold | Minimum number of logs to send                                                          | 5                     |
+| userId            | User identifier                                                                         | null                  |
+| sessionId         | Session identifier                                                                      | null                  |
+| version           | Application version identifier                                                          | null                  |
+| logDetails        | Toggle detailed logs (keys pressed and input/change values)                             | false                 |
+| resolution        | Delay between instances of high frequency logs (mouseover, scroll, etc.)                | 500 (ms)              |
+| userFromParams    | Query param in the page URL to fetch userId from                                        | null                  |
+| toolName          | Name of tool being logged (`service.name` in OTLP)                                      | null                  |
+| authHeader        | `Authorization` header string or callback function                                      | null                  |
+| apiKey            | API key sent via the `x-api-key` HTTP request header                                    | null                  |
+| headers           | Custom HTTP headers object (`Record<string, string>`)                                   | null                  |
+| exporterType      | Wire format / transport (`'http-json'`, `'otlp'`, `'proto-json'`, `'websocket'`)        | `'http-json'`         |
+| otlpEndpoint      | OpenTelemetry Collector HTTP endpoint (auto-appends `/v1/logs` when path is empty/root) | null                  |
+| exporter          | Custom `LogExporter` instance overriding built-in exporters                             | null                  |
+| traceContext      | Callback returning active W3C `{ traceId, spanId }` for OTLP correlation                | null                  |
 
 If you have included UserALE as a `script-tag` in your project, you can use HTML data parameters to pass configuration options to the library through the script tag. For example:
 
 ```html
-  <script
-          src="./node_modules/flagon-userale/build/userale-2.4.0.min.js"
-          data-url="http://localhost:8000/"
-          data-user="example-user"
-          data-version="2.4.0"
-          data-tool="Apache UserALE Example"
-  ></script>
+<script
+  src="./node_modules/flagon-userale/build/main.global.js"
+  data-url="http://localhost:8000/"
+  data-user="example-user"
+  data-version="2.5.0"
+  data-tool="Apache UserALE Example"
+></script>
 ```
 
 You have access to the same parameters listed above, however, naming conventions vary slightly for use in HTML:
 
-| Param | Description | Default |
-|---|---|---|
-| data-url | Logging URL | http://localhost:8000 |
-| data-autostart | Should UserALE start on page load | true |
-| data-interval | Delay between transmit checks | 5000 (ms) |
-| data-threshold | Minimum number of logs to send | 5 |
-| data-user | User identifier | null |
-| data-version | Application version identifier | null |
-| data-log-details | Toggle detailed logs (keys pressed and input/change values) | false |
-| data-resolution | Delay between instances of high frequency logs (mouseover, scroll, etc.) | 500 (ms) |
-| data-user-from-params | Query param in the page URL to fetch userId from | null |
-| data-tool | Name of tool being logged | null |
-| data-auth | Authorization header to be passed to logging endpoint | null |
+| Param                 | Description                                                              | Default               |
+| --------------------- | ------------------------------------------------------------------------ | --------------------- |
+| data-url              | Logging URL                                                              | http://localhost:8000 |
+| data-autostart        | Should UserALE start on page load                                        | true                  |
+| data-interval         | Delay between transmit checks                                            | 5000 (ms)             |
+| data-threshold        | Minimum number of logs to send                                           | 5                     |
+| data-user             | User identifier                                                          | null                  |
+| data-version          | Application version identifier                                           | null                  |
+| data-log-details      | Toggle detailed logs (keys pressed and input/change values)              | false                 |
+| data-resolution       | Delay between instances of high frequency logs (mouseover, scroll, etc.) | 500 (ms)              |
+| data-user-from-params | Query param in the page URL to fetch userId from                         | null                  |
+| data-tool             | Name of tool being logged                                                | null                  |
+| data-auth             | `Authorization` header value                                             | null                  |
+| data-api-key          | `x-api-key` header value                                                 | null                  |
+| data-headers          | JSON-encoded object of custom HTTP headers                               | null                  |
+| data-exporter         | Exporter format (`http-json`, `otlp`, `proto-json`, `websocket`)         | `http-json`           |
+| data-otlp-endpoint    | OpenTelemetry Collector HTTP endpoint                                    | null                  |
 
 If you are using our [WebExtension](https://github.com/apache/flagon/blob/master/products/userale/packages/flagon-userale-ext/),
-you can modify some of these parameters via the extensions' `options` page.
+you can modify these parameters (including OAuth/API Key authentication and Exporter Format) via the extension's `Options` page.
+
+### Authentication, Custom Headers, CORS & WebSocket Limitations
+
+UserALE supports static and dynamic HTTP authentication headers across both interval transmissions and `pagehide` (`keepalive: true`) flushes:
+
+```js
+import * as userale from "flagon-userale";
+
+// 1. Static API key (sent as x-api-key header) or Authorization header
+userale.options({
+  url: "https://logs.example.com/ingest",
+  apiKey: "my-api-key",
+  authHeader: "Bearer my-jwt-token",
+});
+
+// 2. Dynamic token rotation via callback
+userale.registerAuthCallback(() => `Bearer ${getCurrentAccessToken()}`);
+```
+
+- **HTTPS Security**: Always use `https://` endpoints in production when `apiKey` or `authHeader` is configured. UserALE logs a console warning if credentials are configured over plaintext `http://` to a non-localhost host, and automatically strips sensitive `data-api-key`, `data-auth`, `data-token`, `data-secret`, and `data-password` attributes from captured DOM element logs.
+- **CORS Preflight Requirements**: Sending `Content-Type: application/json`, `Authorization`, or `x-api-key` from a browser triggers a CORS `OPTIONS` preflight request. Your ingestion server or OpenTelemetry Collector must include `Content-Type`, `Authorization`, and `x-api-key` in `Access-Control-Allow-Headers`.
+- **WebSocket (`ws://` / `wss://`) Limitation**: The standard browser `WebSocket` API does not allow setting custom HTTP headers (`Authorization` or `x-api-key`) during the WebSocket handshake. When streaming over WebSockets, authenticate via short-lived query parameters in `url` (`wss://logs.example.com/ws?token=...`) or same-site HTTP-only cookies.
+
+### OpenTelemetry (OTLP/JSON & OTel JS SDK) and Pluggable Exporters
+
+UserALE.js natively exports behavioral logs in **OpenTelemetry OTLP/JSON** (`ExportLogsServiceRequest`) or **Apache Flagon Protobuf v1 JSON** (`apache.flagon.userale.v1.LogBatch`) with zero external runtime dependencies:
+
+```js
+import * as userale from "flagon-userale";
+
+// Export directly to an OpenTelemetry Collector (/v1/logs is appended automatically)
+userale.options({
+  exporterType: "otlp",
+  otlpEndpoint: "http://localhost:4318",
+  toolName: "checkout-web",
+  toolVersion: "1.4.0",
+  // Optional: correlate DOM events with active W3C TraceContext spans
+  traceContext: () => ({
+    traceId: "4bf92f3577b34da6a3ce929d0e0e4736",
+    spanId: "00f067aa0ba902b7",
+  }),
+});
+```
+
+If your application already initializes the OpenTelemetry JS Logs SDK (`@opentelemetry/sdk-logs`), bridge UserALE into your existing `Logger` using `OtelSdkExporter`:
+
+```js
+import * as userale from "flagon-userale";
+import { logs } from "@opentelemetry/api-logs";
+
+const otelLogger = logs.getLogger("userale");
+userale.options({
+  exporter: new userale.OtelSdkExporter(otelLogger),
+});
+```
+
+See [`example/otel-demo`](https://github.com/apache/flagon/tree/master/products/userale/example/otel-demo) for a complete Docker Compose stack with **OpenTelemetry Collector**, **Grafana Loki**, **Grafana Tempo**, and a pre-built **Grafana Dashboard** featuring click-to-trace correlation.
 
 ## Usage
 
@@ -198,21 +252,21 @@ We have exposed a number of functions that assist you in modifying, filtering, a
 
 A complete list of available functions are as follows:
 
-| Function | Description | Notes |
-|---|---|---|
-| userale.options | modify userale's configuration option | see top level README for complete list of options |
-| [DEPRECATED] userale.filter | filters out logs from logging queue by keys or values | filters are callbacks with global scope |
-| [DEPRECATED] userale.map | modify/add log keys or values | mappings are callbacks with global scope |
-| userale.addCallbacks | add one or more callbacks to be executed during log packaging | callbacks have global scope |
-| userale.removeCallbacks | remove one or more callbacks by name | Removes callbacks added from userale.addCallbacks |
-| userale.log | appends a custom log to the log queue | the custom log object is an object key:value pairs |
-| userale.packageLog | transforms the provided event into a log and appends it to the log queue | designed for HTML events |
-| userale.packageCustomLog | packages the provided customLog to include standard meta data and appends it to the log queue | designed for non HTML events|
-| userale.details | defines the way information is extracted from various events | supports packageLog/packageCustomLog 'details' |
-| userale.getSelector | builds a string CSS selector from the provided HTML element id | populates 'target' field in packaged logs |
-| userale.buildPath| builds an array of elements from the provided event target, to the root element (DOM path) | populates the 'path' field in packaged logs |
-| userale.start | used to start the logging process if | unecessary if 'autostart' is set to true in initial setting (default) |
-| userale.stop | halts the logging process. Logs will no longer be sent | will need to invoke userale.start to restart logging |
+| Function                    | Description                                                                                   | Notes                                                                 |
+| --------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
+| userale.options             | modify userale's configuration option                                                         | see top level README for complete list of options                     |
+| [DEPRECATED] userale.filter | filters out logs from logging queue by keys or values                                         | filters are callbacks with global scope                               |
+| [DEPRECATED] userale.map    | modify/add log keys or values                                                                 | mappings are callbacks with global scope                              |
+| userale.addCallbacks        | add one or more callbacks to be executed during log packaging                                 | callbacks have global scope                                           |
+| userale.removeCallbacks     | remove one or more callbacks by name                                                          | Removes callbacks added from userale.addCallbacks                     |
+| userale.log                 | appends a custom log to the log queue                                                         | the custom log object is an object key:value pairs                    |
+| userale.packageLog          | transforms the provided event into a log and appends it to the log queue                      | designed for HTML events                                              |
+| userale.packageCustomLog    | packages the provided customLog to include standard meta data and appends it to the log queue | designed for non HTML events                                          |
+| userale.details             | defines the way information is extracted from various events                                  | supports packageLog/packageCustomLog 'details'                        |
+| userale.getSelector         | builds a string CSS selector from the provided HTML element id                                | populates 'target' field in packaged logs                             |
+| userale.buildPath           | builds an array of elements from the provided event target, to the root element (DOM path)    | populates the 'path' field in packaged logs                           |
+| userale.start               | used to start the logging process if                                                          | unecessary if 'autostart' is set to true in initial setting (default) |
+| userale.stop                | halts the logging process. Logs will no longer be sent                                        | will need to invoke userale.start to restart logging                  |
 
 Including UserALE as a `script-tag` provides you access to the same functions listed above. However, UserALE essentially
 becomes a property of the DOM. As such, you'll need to call functions as a window property:
@@ -231,24 +285,18 @@ Select examples are below:
 Filter your logs with `userale.filter`:
 
 ```html
-userale.filter(function (log) {
-    var type_array = ['mouseup', 'mouseover', 'mousedown', 'keydown', 'dblclick', 'blur', 'focus', 'input', 'wheel'];
-    var logType_array = ['interval'];
-    return !type_array.includes(log.type) && !logType_array.includes(log.logType);
-});
+userale.filter(function (log) { var type_array = ['mouseup', 'mouseover',
+'mousedown', 'keydown', 'dblclick', 'blur', 'focus', 'input', 'wheel']; var
+logType_array = ['interval']; return !type_array.includes(log.type) &&
+!logType_array.includes(log.logType); });
 ```
 
 Modify (add/remove) log fields with surgical precision using `userale.map`:
 
 ```html
-userale.map(function (log) {
-        var targetsForLabels = ["button#test_button"];
-        if (targetsForLabels.includes(log.target)) {
-            return Object.assign({}, log, { CustomLabel: "Click me!" });
-        } else {
-            return log;
-        }
-      });
+userale.map(function (log) { var targetsForLabels = ["button#test_button"]; if
+(targetsForLabels.includes(log.target)) { return Object.assign({}, log, {
+CustomLabel: "Click me!" }); } else { return log; } });
 ```
 
 (Additional examples illustrate [precision custom labeling](https://github.com/apache/flagon/tree/master/products/userale/packages/flagon-userale/example/log-label-example), using a variety of functions.)
@@ -256,48 +304,27 @@ userale.map(function (log) {
 Generate custom logs with `userale.log`:
 
 ```html
-document.addEventListener('change', function(e) {
-    if (e.target.value === 'log') {
-        userale.log({
-            target: userale.getSelector(e.target),
-            path: userale.buildPath(e),
-            type: e.type,
-            logType: 'custom',
-            userAction: false,
-            details: 'I can make this log look like anything I want',
-            customField1: 'foo',
-            customField2: 'bar',
-            userId: userale.options().userId,
-            toolVersion: userale.options().version,
-            toolName: userale.options().toolName,
-            useraleVersion: userale.options().useraleVersion,
-            sessionId: userale.options().sessionId,
-            customLabel: "(custom) Log Example"
-        });
-    }
-});
+document.addEventListener('change', function(e) { if (e.target.value === 'log')
+{ userale.log({ target: userale.getSelector(e.target), path:
+userale.buildPath(e), type: e.type, logType: 'custom', userAction: false,
+details: 'I can make this log look like anything I want', customField1: 'foo',
+customField2: 'bar', userId: userale.options().userId, toolVersion:
+userale.options().version, toolName: userale.options().toolName, useraleVersion:
+userale.options().useraleVersion, sessionId: userale.options().sessionId,
+customLabel: "(custom) Log Example" }); } });
 ```
 
 User our own log packaging pipeline to streamline custom HTML event logging with `userale.packageLog`:
 
 ```html
-document.addEventListener('change', function(e){
-    if (e.target.value === 'packageLog') {
-        /**You can then use the 'Mapping' API function to modify custom logs created with the packageLog function*/
-        userale.map(function (log) {
-            var targetsForLabels = ['change'];
-            if (targetsForLabels.includes(log.type)) {
-                return Object.assign({}, log, { logType: 'custom', customLabel: 'packageLog Example' });
-            } else {
-                return log;
-            }
-        });
-        /**You can also use the details function to package additional log meta data, or add custom details*/
-        userale.packageLog(e, userale.details(userale.options(),'change'));
-    } else {
-        return false
-    }
-});
+document.addEventListener('change', function(e){ if (e.target.value ===
+'packageLog') { /**You can then use the 'Mapping' API function to modify custom
+logs created with the packageLog function*/ userale.map(function (log) { var
+targetsForLabels = ['change']; if (targetsForLabels.includes(log.type)) { return
+Object.assign({}, log, { logType: 'custom', customLabel: 'packageLog Example'
+}); } else { return log; } }); /**You can also use the details function to
+package additional log meta data, or add custom details*/ userale.packageLog(e,
+userale.details(userale.options(),'change')); } else { return false } });
 ```
 
 Again, see [Usage](#usage) for differences in invoking these functions with `module` and `script-tag` includes.
@@ -322,11 +349,13 @@ npm run build
 ```
 
 To run UserALE unit tests:
+
 ```
 npm run test
 ```
 
 We use gulp-mocha for unit tests. The results will print to your terminal:
+
 ```
 ...
     attachHandlers
@@ -339,13 +368,14 @@ We use gulp-mocha for unit tests. The results will print to your terminal:
   45 passing (954ms)
   1 pending
 ```
+
 Any failing tests will also be logged in the terminal. If there are failing tests, please consider [submitting an issue report](https://github.com/apache/flagon/issues).
 
 For more guidance on modifying Flagon UserALE src code, check out [the guide on our website](http://flagon.apache.org/docs/useralejs/modifying/).
 
 ## Contributing
 
-Contributions are welcome!  Simply [submit an issue](https://github.com/apache/flagon/issues). Pull requests are welcome.  The core team will review it and work with you to incorporate it into UserALE. If you want to become a contributor to the project, see our [contribution guide](http://flagon.apache.org/docs/contributing/).
+Contributions are welcome! Simply [submit an issue](https://github.com/apache/flagon/issues). Pull requests are welcome. The core team will review it and work with you to incorporate it into UserALE. If you want to become a contributor to the project, see our [contribution guide](http://flagon.apache.org/docs/contributing/).
 
 Join the conversation: tell us your needs, wishes, and interests by joining our [mailing list](dev-subscribe@flagon.apache.org)!