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 23:19:37 UTC
(flagon) branch fix/pre-release-audit-and-ext created (now dbf5dce)
This is an automated email from the ASF dual-hosted git repository.
brucearctor pushed a change to branch fix/pre-release-audit-and-ext
in repository https://gitbox.apache.org/repos/asf/flagon.git
at dbf5dce fix: resolve pre-release audit bugs, browser detection, extension filters, site CSP/links, and DOAP
This branch includes the following new commits:
new dbf5dce fix: resolve pre-release audit bugs, browser detection, extension filters, site CSP/links, and DOAP
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(flagon) 01/01: fix: resolve pre-release audit bugs, browser detection, extension filters, site CSP/links, and DOAP
Posted by au...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
brucearctor pushed a commit to branch fix/pre-release-audit-and-ext
in repository https://gitbox.apache.org/repos/asf/flagon.git
commit dbf5dce508582e2fff8e9d0c18a6ddc5423d6c83
Author: Bruce Arctor <50...@users.noreply.github.com>
AuthorDate: Sun Oct 11 16:18:36 2026 -0700
fix: resolve pre-release audit bugs, browser detection, extension filters, site CSP/links, and DOAP
---
doap_flagon.rdf | 39 +++++-----
products/distill/distill/analytics/graph/graph.py | 2 +-
products/distill/distill/core/log.py | 6 +-
products/distill/distill/segmentation/segment.py | 4 +-
products/distill/tests/test_log.py | 20 ++++++
products/distill/tests/testing_utils.py | 2 +
.../src/background/messages/config_change.ts | 41 ++++++++++-
.../flagon-userale-ext/src/background/ports/log.ts | 11 ++-
.../flagon-userale-ext/src/options/logging.tsx | 36 +++++++++-
.../flagon-userale-ext/src/utils/storage.ts | 13 +++-
.../userale/packages/flagon-userale/package.json | 2 +-
.../packages/flagon-userale/src/configure.ts | 12 ++++
.../packages/flagon-userale/src/exporters/index.ts | 9 ++-
.../flagon-userale/src/exporters/proto-json.ts | 1 +
.../packages/flagon-userale/src/exporters/types.ts | 8 ++-
.../flagon-userale/src/getInitialSettings.ts | 3 +
.../userale/packages/flagon-userale/src/main.ts | 39 +++++++---
.../packages/flagon-userale/src/packageLogs.ts | 82 ++++++++++++++++------
.../packages/flagon-userale/src/sendLogs.ts | 33 ++++++++-
.../userale/packages/flagon-userale/src/types.d.ts | 1 +
.../flagon-userale/src/utils/headers/index.ts | 2 +-
.../flagon-userale/test/spec/exporters.spec.ts | 15 ++++
.../flagon-userale/test/spec/headers.spec.ts | 15 +++-
.../flagon-userale/test/spec/packageLogs.spec.ts | 7 ++
products/userale/test/playwright.config.ts | 17 +++--
.../test/spec/fixtures/extension.fixture.ts | 35 +++++----
site/_includes/footer.html | 18 ++---
site/_includes/useralejsDemo.html | 4 +-
site/_pages/community.html | 4 +-
site/_pages/releases.html | 8 +--
site/semantic/dist/components/site.css | 1 -
site/semantic/dist/components/site.min.css | 2 +-
site/semantic/dist/semantic.css | 4 +-
site/semantic/dist/semantic.min.css | 2 +-
34 files changed, 380 insertions(+), 118 deletions(-)
diff --git a/doap_flagon.rdf b/doap_flagon.rdf
index 9e42596..af1cec4 100755
--- a/doap_flagon.rdf
+++ b/doap_flagon.rdf
@@ -24,40 +24,39 @@
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:asfext="http://projects.apache.org/ns/asfext#"
xmlns:foaf="http://xmlns.com/foaf/0.1/">
-<!--
- 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
-
- https://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.
--->
<Project rdf:about="https://flagon.apache.org/">
<created>2024-04-10</created>
<license rdf:resource="https://spdx.org/licenses/Apache-2.0" />
<name>Apache Flagon</name>
<homepage rdf:resource="https://flagon.apache.org/" />
<asfext:pmc rdf:resource="https://flagon.apache.org/" />
- <shortdesc>Flagon includes both UserALE, an instrumentation tool for web applications, and Distill, a python library for analyzing UserALE telemetry.</shortdesc>
- <description>Flagon includes both UserALE, an instrumentation tool for web applications, and Distill, a python library for analyzing UserALE telemetry.</description>
+ <shortdesc>Apache Flagon is an open-source software usability, behavioral logging, and telemetry analytics suite comprising UserALE.js and Distill.</shortdesc>
+ <description>Apache Flagon is a software toolset for thin-client behavioral logging, user experience telemetry, and behavioral analytics. It includes UserALE.js (a client-side JavaScript/TypeScript DOM instrumentation library and Manifest V3 WebExtension supporting OpenTelemetry OTLP and Protobuf v1 schemas) and Distill (a Python analytics and graph-modeling library for analyzing UserALE and OpenTelemetry user interaction logs).</description>
<bug-database rdf:resource="https://github.com/apache/flagon/issues" />
<mailing-list rdf:resource="https://lists.apache.org/list.html?dev@flagon.apache.org" />
- <download-page rdf:resource="https://dist.apache.org/repos/dist/release/flagon/" />
+ <download-page rdf:resource="https://flagon.apache.org/releases/" />
<programming-language>JavaScript</programming-language>
<programming-language>Python</programming-language>
- <programming-language>Typescript</programming-language>
+ <programming-language>TypeScript</programming-language>
<category rdf:resource="https://projects.apache.org/category/observability" />
<category rdf:resource="https://projects.apache.org/category/testing" />
<category rdf:resource="https://projects.apache.org/category/python" />
+ <category rdf:resource="https://projects.apache.org/category/javascript" />
<category rdf:resource="https://projects.apache.org/category/data-visualization" />
+ <release>
+ <Version>
+ <name>Apache Flagon UserALE.js</name>
+ <created>2024-02-23</created>
+ <revision>2.4.0</revision>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <name>Apache Flagon Distill</name>
+ <created>2024-02-19</created>
+ <revision>0.1.0</revision>
+ </Version>
+ </release>
<repository>
<GitRepository>
<location rdf:resource="https://github.com/apache/flagon.git"/>
diff --git a/products/distill/distill/analytics/graph/graph.py b/products/distill/distill/analytics/graph/graph.py
index 016221e..98d704e 100644
--- a/products/distill/distill/analytics/graph/graph.py
+++ b/products/distill/distill/analytics/graph/graph.py
@@ -210,7 +210,7 @@ def funnel(edges, targets, node_labels=False, *, infer=True):
targets.extend(path)
else:
# Otherwise construct a path literally
- target_edges = nx.utils.pairwise(targets)
+ target_edges = list(nx.utils.pairwise(targets))
# Get the total outflow of the starting node
counts = sum(
diff --git a/products/distill/distill/core/log.py b/products/distill/distill/core/log.py
index e114a18..ba89665 100644
--- a/products/distill/distill/core/log.py
+++ b/products/distill/distill/core/log.py
@@ -38,7 +38,9 @@ class Log:
def __init__(self, data: Union[str, JsonDict], schema=None):
if schema is None:
schema = UserAleSchema
- elif issubclass(schema, BaseModel):
+ elif isinstance(schema, type) and issubclass(schema, BaseModel):
+ schema = TypeAdapter(schema)
+ elif not isinstance(schema, TypeAdapter):
raise TypeError("schema should inherit from pydantic.BaseModel")
if isinstance(data, str):
@@ -82,7 +84,7 @@ class Log:
def __ge__(self, other):
if isinstance(other, Log):
- return self.id > other.id
+ return self.id >= other.id
if isinstance(other, datetime):
return self.id.get_datetime() >= other
diff --git a/products/distill/distill/segmentation/segment.py b/products/distill/distill/segmentation/segment.py
index a8d4e5a..fba7207 100644
--- a/products/distill/distill/segmentation/segment.py
+++ b/products/distill/distill/segmentation/segment.py
@@ -38,7 +38,7 @@ class Segment:
Distill's segmentation package. Allows the user to segment User Ale log data.
"""
- def __init__(self, segment_name="", start_end_val=None, num_logs=0, uids=[]):
+ def __init__(self, segment_name="", start_end_val=None, num_logs=0, uids=None):
"""
Initializes a Segment object. This object contains\
metadata for the associated Segment.
@@ -56,7 +56,7 @@ class Segment:
self.segment_name = segment_name
self.start_end_val = start_end_val
self.num_logs = num_logs
- self.uids = uids
+ self.uids = [] if uids is None else uids
self.generate_field_name = None
self.generate_matched_values = None
self.segment_type = None
diff --git a/products/distill/tests/test_log.py b/products/distill/tests/test_log.py
index 78f803f..753b8f5 100644
--- a/products/distill/tests/test_log.py
+++ b/products/distill/tests/test_log.py
@@ -252,6 +252,26 @@ def test_log_v2_5_and_wire_formats():
assert otlp_log.to_dict()["traceId"] == "4bf92f3577b34da6a3ce929d0e0e4736"
+def test_audit_bug_fixes():
+ from distill.analytics.graph.graph import funnel
+ from distill.schemas.userale import UserAleRawSchema
+ from distill.segmentation.segment import Segment
+
+ data = load_log()
+ log1 = Log(data=data, schema=UserAleRawSchema)
+ assert log1 >= log1
+ assert log1 <= log1
+
+ s1 = Segment("s1", (0, 10))
+ s2 = Segment("s2", (0, 10))
+ s1.uids.append("u1")
+ assert s2.uids == []
+
+ edges = [("a", "b"), ("b", "c"), ("a", "b")]
+ fig = funnel(edges, ["a", "b", "c"], infer=False)
+ assert fig is not None
+
+
def load_log() -> str:
with open(os.path.join(DATA_DIR, "log_test_data.json")) as f:
data = f.readline()
diff --git a/products/distill/tests/testing_utils.py b/products/distill/tests/testing_utils.py
index b0d8cf3..0f7b43c 100644
--- a/products/distill/tests/testing_utils.py
+++ b/products/distill/tests/testing_utils.py
@@ -52,4 +52,6 @@ def setup(file, date_type):
def to_datetime(date):
+ if isinstance(date, str) and date.isdigit():
+ date = int(date)
return pd.to_datetime(date, unit="ms", origin="unix")
diff --git a/products/userale/packages/flagon-userale-ext/src/background/messages/config_change.ts b/products/userale/packages/flagon-userale-ext/src/background/messages/config_change.ts
index 667b557..f265d5c 100644
--- a/products/userale/packages/flagon-userale-ext/src/background/messages/config_change.ts
+++ b/products/userale/packages/flagon-userale-ext/src/background/messages/config_change.ts
@@ -21,13 +21,30 @@ import * as userale from "flagon-userale"
import type { PlasmoMessaging } from "@plasmohq/messaging"
-import { getStoredOptions, type StoredOptions } from "~utils/storage"
+import {
+ DEFAULT_OPTIONS,
+ getStoredOptions,
+ type StoredOptions
+} from "~utils/storage"
-let allowListRegExp: RegExp
+let allowListRegExp: RegExp = new RegExp(DEFAULT_OPTIONS.allowList)
+let excludedEventTypes = new Set<string>()
+let excludedLogTypes = new Set<string>()
-const handler: PlasmoMessaging.MessageHandler = async (req) => {
+function parseCsvSet(raw?: string): Set<string> {
+ if (!raw) return new Set()
+ return new Set(
+ raw
+ .split(",")
+ .map((s) => s.trim().toLowerCase())
+ .filter(Boolean)
+ )
+}
+
+const handler: PlasmoMessaging.MessageHandler = async (req, res) => {
const stored = await getStoredOptions()
setOptions({ ...stored, ...req.body })
+ res.send("Configuration saved!")
}
export function setOptions(options: StoredOptions) {
@@ -36,6 +53,8 @@ export function setOptions(options: StoredOptions) {
exporterType: options.exporterType ?? "http-json"
})
allowListRegExp = new RegExp(options.allowList)
+ excludedEventTypes = parseCsvSet(options.filterEventTypes)
+ excludedLogTypes = parseCsvSet(options.filterLogTypes)
switch (options.authMode) {
case "oauth":
@@ -61,4 +80,20 @@ export function getAllowListRegExp() {
return allowListRegExp
}
+export function shouldLogByFilter(log: Record<string, any>): boolean {
+ if (
+ typeof log.type === "string" &&
+ excludedEventTypes.has(log.type.toLowerCase())
+ ) {
+ return false
+ }
+ if (
+ typeof log.logType === "string" &&
+ excludedLogTypes.has(log.logType.toLowerCase())
+ ) {
+ return false
+ }
+ return true
+}
+
export default handler
diff --git a/products/userale/packages/flagon-userale-ext/src/background/ports/log.ts b/products/userale/packages/flagon-userale-ext/src/background/ports/log.ts
index a5aea48..64e66a4 100644
--- a/products/userale/packages/flagon-userale-ext/src/background/ports/log.ts
+++ b/products/userale/packages/flagon-userale-ext/src/background/ports/log.ts
@@ -21,13 +21,20 @@ import * as userale from "flagon-userale"
import type { PlasmoMessaging } from "@plasmohq/messaging"
-import { getAllowListRegExp } from "~/background/messages/config_change"
+import {
+ getAllowListRegExp,
+ shouldLogByFilter
+} from "~/background/messages/config_change"
const handler: PlasmoMessaging.PortHandler = async (req, res) => {
const log = req.body
log.browserSessionId = browserSessionId
const allowListRegExp = getAllowListRegExp()
- if (allowListRegExp.test(log.pageUrl)) {
+ if (
+ allowListRegExp &&
+ allowListRegExp.test(log.pageUrl) &&
+ shouldLogByFilter(log)
+ ) {
console.log(log)
userale.log(log)
}
diff --git a/products/userale/packages/flagon-userale-ext/src/options/logging.tsx b/products/userale/packages/flagon-userale-ext/src/options/logging.tsx
index f163a65..2867eb1 100644
--- a/products/userale/packages/flagon-userale-ext/src/options/logging.tsx
+++ b/products/userale/packages/flagon-userale-ext/src/options/logging.tsx
@@ -30,12 +30,16 @@ function Logging() {
const [allowList, setAllowList] = useState("")
const [exporterType, setExporterType] =
useState<StoredOptions["exporterType"]>("http-json")
+ const [filterEventTypes, setFilterEventTypes] = useState("")
+ const [filterLogTypes, setFilterLogTypes] = useState("")
useEffect(() => {
getStoredOptions().then((data) => {
setLoggingUrl(data.loggingUrl)
setAllowList(data.allowList)
setExporterType(data.exporterType)
+ setFilterEventTypes(data.filterEventTypes)
+ setFilterLogTypes(data.filterLogTypes)
})
}, [])
@@ -44,9 +48,15 @@ function Logging() {
const resp = await setStoredOptions({
loggingUrl,
allowList,
- exporterType
+ exporterType,
+ filterEventTypes,
+ filterLogTypes
})
- alert(resp)
+ if (resp instanceof Error) {
+ alert(resp.message)
+ } else {
+ alert(typeof resp === "string" && resp ? resp : "Configuration saved!")
+ }
}
return (
@@ -82,6 +92,28 @@ function Logging() {
/>
<br />
+ <label htmlFor="filter-event-types">
+ Excluded Event Types (comma-separated, e.g. mouseover,wheel):
+ </label>
+ <input
+ id="filter-event-types"
+ value={filterEventTypes}
+ placeholder="mouseover,mouseup,mousedown"
+ onChange={(e) => setFilterEventTypes(e.target.value)}
+ />
+ <br />
+
+ <label htmlFor="filter-log-types">
+ Excluded Log Types (comma-separated, e.g. interval,raw,custom):
+ </label>
+ <input
+ id="filter-log-types"
+ value={filterLogTypes}
+ placeholder="interval"
+ onChange={(e) => setFilterLogTypes(e.target.value)}
+ />
+ <br />
+
<div style={{ textAlign: "right" }}>
<button type="submit">Save Changes</button>
</div>
diff --git a/products/userale/packages/flagon-userale-ext/src/utils/storage.ts b/products/userale/packages/flagon-userale-ext/src/utils/storage.ts
index 49e70b1..52bd7f3 100644
--- a/products/userale/packages/flagon-userale-ext/src/utils/storage.ts
+++ b/products/userale/packages/flagon-userale-ext/src/utils/storage.ts
@@ -27,6 +27,8 @@ export const STORAGE_KEYS = {
authMode: "authMode",
allowList: "allowList",
exporterType: "exporterType",
+ filterEventTypes: "filterEventTypes",
+ filterLogTypes: "filterLogTypes",
loggingUrl: "loggingUrl"
} as const
@@ -37,16 +39,20 @@ export type StoredOptions = {
apiKey: string
authMode: "oauth" | "apikey" | "none"
allowList: string
- exporterType: "http-json" | "otlp" | "proto-json"
+ exporterType: "http-json" | "v2-json" | "otlp" | "otlp-json" | "proto-json"
+ filterEventTypes: string
+ filterLogTypes: string
loggingUrl: string
}
-const DEFAULT_OPTIONS: StoredOptions = {
+export const DEFAULT_OPTIONS: StoredOptions = {
accessToken: "",
apiKey: "",
authMode: "none",
allowList: "https://flagon.apache.org/",
exporterType: "http-json",
+ filterEventTypes: "",
+ filterLogTypes: "",
loggingUrl: "http://localhost:8000"
}
@@ -61,6 +67,9 @@ export async function getStoredOptions(): Promise<StoredOptions> {
authMode: stored.authMode ?? DEFAULT_OPTIONS.authMode,
allowList: stored.allowList ?? DEFAULT_OPTIONS.allowList,
exporterType: stored.exporterType ?? DEFAULT_OPTIONS.exporterType,
+ filterEventTypes:
+ stored.filterEventTypes ?? DEFAULT_OPTIONS.filterEventTypes,
+ filterLogTypes: stored.filterLogTypes ?? DEFAULT_OPTIONS.filterLogTypes,
loggingUrl: stored.loggingUrl ?? DEFAULT_OPTIONS.loggingUrl
}
}
diff --git a/products/userale/packages/flagon-userale/package.json b/products/userale/packages/flagon-userale/package.json
index 9496180..3523533 100644
--- a/products/userale/packages/flagon-userale/package.json
+++ b/products/userale/packages/flagon-userale/package.json
@@ -26,7 +26,7 @@
},
"repository": {
"type": "git",
- "url": "git://https://github.com/apache/flagon-useralejs"
+ "url": "https://github.com/apache/flagon.git"
},
"keywords": [
"UserALE",
diff --git a/products/userale/packages/flagon-userale/src/configure.ts b/products/userale/packages/flagon-userale/src/configure.ts
index afbf946..2c42665 100644
--- a/products/userale/packages/flagon-userale/src/configure.ts
+++ b/products/userale/packages/flagon-userale/src/configure.ts
@@ -31,6 +31,7 @@ export class Configuration {
public browserSessionId: Settings.SessionId = null;
public custIndex: Settings.CustomIndex = null;
public exporter: Settings.CustomExporter = null;
+ public exporterFormat: Settings.ExporterType = "http-json";
public exporterType: Settings.ExporterType = "http-json";
public headers: Settings.Headers = null;
public httpSessionId: Settings.SessionId = null;
@@ -106,6 +107,17 @@ export class Configuration {
const newOption = newConfig[option];
if (newOption !== undefined) {
this[option] = newOption;
+ if (
+ option === "exporterFormat" &&
+ newConfig.exporterType === undefined
+ ) {
+ this.exporterType = newOption as Settings.ExporterType;
+ } else if (
+ option === "exporterType" &&
+ newConfig.exporterFormat === undefined
+ ) {
+ this.exporterFormat = newOption as Settings.ExporterType;
+ }
}
});
}
diff --git a/products/userale/packages/flagon-userale/src/exporters/index.ts b/products/userale/packages/flagon-userale/src/exporters/index.ts
index c690a34..7bb2cb0 100644
--- a/products/userale/packages/flagon-userale/src/exporters/index.ts
+++ b/products/userale/packages/flagon-userale/src/exporters/index.ts
@@ -85,7 +85,9 @@ export function createExporter(config: Configuration): LogExporter {
return config.exporter;
}
- if (config.exporterType === "otlp" || config.otlpEndpoint) {
+ const mode = config.exporterFormat || config.exporterType;
+
+ if (mode === "otlp" || mode === "otlp-json" || config.otlpEndpoint) {
return new OtlpJsonExporter(config);
}
@@ -94,7 +96,8 @@ export function createExporter(config: Configuration): LogExporter {
if (
url.protocol === "ws:" ||
url.protocol === "wss:" ||
- config.exporterType === "websocket"
+ mode === "websocket" ||
+ config.websocketsEnabled
) {
return new WebSocketExporter(config);
}
@@ -102,7 +105,7 @@ export function createExporter(config: Configuration): LogExporter {
// Fallback to HTTP exporter if config.url is not a full URL (e.g. relative path in tests)
}
- if (config.exporterType === "proto-json") {
+ if (mode === "proto-json") {
return new ProtoJsonExporter(config);
}
diff --git a/products/userale/packages/flagon-userale/src/exporters/proto-json.ts b/products/userale/packages/flagon-userale/src/exporters/proto-json.ts
index 45a31e7..664b7b3 100644
--- a/products/userale/packages/flagon-userale/src/exporters/proto-json.ts
+++ b/products/userale/packages/flagon-userale/src/exporters/proto-json.ts
@@ -22,6 +22,7 @@ const KNOWN_CUSTOM_LOG_KEYS = new Set([
"pageUrl",
"pageTitle",
"pageReferrer",
+ "browser",
"userAgent",
"clientTime",
"scrnRes",
diff --git a/products/userale/packages/flagon-userale/src/exporters/types.ts b/products/userale/packages/flagon-userale/src/exporters/types.ts
index 5a1ebcb..539ab00 100644
--- a/products/userale/packages/flagon-userale/src/exporters/types.ts
+++ b/products/userale/packages/flagon-userale/src/exporters/types.ts
@@ -18,7 +18,13 @@
import type { Logging } from "@/types";
export type ExporterType =
- "http-json" | "proto-json" | "otlp" | "websocket" | "custom";
+ | "http-json"
+ | "v2-json"
+ | "proto-json"
+ | "otlp"
+ | "otlp-json"
+ | "websocket"
+ | "custom";
export interface ExportOptions {
keepalive?: boolean;
diff --git a/products/userale/packages/flagon-userale/src/getInitialSettings.ts b/products/userale/packages/flagon-userale/src/getInitialSettings.ts
index 9af090f..55cbe50 100644
--- a/products/userale/packages/flagon-userale/src/getInitialSettings.ts
+++ b/products/userale/packages/flagon-userale/src/getInitialSettings.ts
@@ -39,6 +39,7 @@ export function getInitialSettings(): Settings.Config {
browserSessionId: null,
custIndex: null,
exporter: null,
+ exporterFormat: "http-json",
exporterType: "http-json",
headers: null,
httpSessionId: null,
@@ -89,6 +90,7 @@ export function getInitialSettings(): Settings.Config {
const headers = get("data-headers");
const otlpEndpoint = get("data-otlp-endpoint") || null;
const exporterAttr = (get("data-exporter-type") ||
+ get("data-exporter-format") ||
get("data-exporter") ||
(otlpEndpoint ? "otlp" : "http-json")) as Settings.ExporterType;
const settings: Settings.Config = {
@@ -98,6 +100,7 @@ export function getInitialSettings(): Settings.Config {
browserSessionId: null,
custIndex: get("data-index") || null,
exporter: null,
+ exporterFormat: exporterAttr,
exporterType: exporterAttr,
headers: headers ? JSON.parse(headers) : null,
httpSessionId: httpSessionId,
diff --git a/products/userale/packages/flagon-userale/src/main.ts b/products/userale/packages/flagon-userale/src/main.ts
index eaedbcd..c363537 100644
--- a/products/userale/packages/flagon-userale/src/main.ts
+++ b/products/userale/packages/flagon-userale/src/main.ts
@@ -16,7 +16,12 @@
*/
import { defineCustomDetails } from "@/attachHandlers";
-import { registerAuthCallback } from "@/utils";
+import {
+ registerAuthCallback,
+ registerHeadersCallback,
+ resetAuthCallback,
+ resetHeadersCallback,
+} from "@/utils";
import {
addCallbacks,
removeCallbacks,
@@ -24,12 +29,13 @@ import {
packageCustomLog,
getSelector,
buildPath,
+ detectBrowser,
initPackager,
} from "@/packageLogs";
import { version as userAleVersion } from "../package.json";
import { Configuration } from "@/configure";
import { attachHandlers } from "@/attachHandlers";
-import { initSender } from "@/sendLogs";
+import { initSender, registerExporter } from "@/sendLogs";
import type { Settings, Logging } from "@/types";
@@ -37,16 +43,24 @@ const config = Configuration.getInstance();
const logs: Array<Logging.Log> = [];
const startLoadTimestamp = Date.now();
-let endLoadTimestamp: number;
-if (typeof self !== "undefined") {
- self.onload = function () {
+let endLoadTimestamp: number | undefined;
+if (
+ typeof self !== "undefined" &&
+ typeof self.addEventListener === "function"
+) {
+ self.addEventListener("load", function () {
endLoadTimestamp = Date.now();
- };
+ });
}
export let started = false;
export { defineCustomDetails as details } from "@/attachHandlers";
-export { registerAuthCallback as registerAuthCallback } from "@/utils";
+export {
+ registerAuthCallback,
+ registerHeadersCallback,
+ resetAuthCallback,
+ resetHeadersCallback,
+} from "@/utils";
export {
addCallbacks as addCallbacks,
removeCallbacks as removeCallbacks,
@@ -54,7 +68,9 @@ export {
packageCustomLog as packageCustomLog,
getSelector as getSelector,
buildPath as buildPath,
+ detectBrowser as detectBrowser,
} from "@/packageLogs";
+export { registerExporter } from "@/sendLogs";
export {
buildHeaders,
buildOtlpResourceAttributes,
@@ -117,10 +133,14 @@ function setup(config: Configuration) {
initSender(logs, config);
started = config.on = true;
if (typeof window !== "undefined" && typeof document !== "undefined") {
+ const loadDuration = Math.max(
+ 1,
+ (endLoadTimestamp ?? Date.now()) - startLoadTimestamp,
+ );
packageCustomLog(
{
type: "load",
- details: { pageLoadTime: endLoadTimestamp - startLoadTimestamp },
+ details: { pageLoadTime: loadDuration },
},
() => ({}),
false,
@@ -200,11 +220,14 @@ if (typeof window !== "undefined") {
version: userAleVersion,
details: defineCustomDetails,
registerAuthCallback,
+ registerHeadersCallback,
+ registerExporter,
addCallbacks,
removeCallbacks,
packageLog,
packageCustomLog,
getSelector,
buildPath,
+ detectBrowser,
};
}
diff --git a/products/userale/packages/flagon-userale/src/packageLogs.ts b/products/userale/packages/flagon-userale/src/packageLogs.ts
index 3a3c879..af0e148 100644
--- a/products/userale/packages/flagon-userale/src/packageLogs.ts
+++ b/products/userale/packages/flagon-userale/src/packageLogs.ts
@@ -91,6 +91,57 @@ export function initPackager(
intervalLog = null;
}
+/**
+ * Zero-dependency browser metadata detection (#108).
+ * Uses User-Agent Client Hints (navigator.userAgentData) when available,
+ * falling back to lightweight User-Agent string parsing.
+ */
+export function detectBrowser(): {
+ browser: string | null;
+ version: string | null;
+} {
+ try {
+ const nav = typeof self !== "undefined" ? (self.navigator as any) : null;
+ if (!nav) {
+ return { browser: null, version: null };
+ }
+ const brands: Array<{ brand: string; version: string }> | undefined =
+ nav.userAgentData?.brands;
+ if (Array.isArray(brands) && brands.length > 0) {
+ const meaningful =
+ brands.find(
+ (b) => b.brand && !/not.?a.?brand|chromium/i.test(b.brand),
+ ) ?? brands[0];
+ if (meaningful?.brand) {
+ return {
+ browser: meaningful.brand.toLowerCase(),
+ version: meaningful.version || null,
+ };
+ }
+ }
+ const ua: string = typeof nav.userAgent === "string" ? nav.userAgent : "";
+ if (!ua) {
+ return { browser: null, version: null };
+ }
+ const rules: Array<[string, RegExp]> = [
+ ["edge", /Edg(?:e|A|iOS)?\/([\d.]+)/],
+ ["opera", /(?:OPR|Opera)\/([\d.]+)/],
+ ["chrome", /(?:Chrome|CriOS)\/([\d.]+)/],
+ ["firefox", /(?:Firefox|FxiOS)\/([\d.]+)/],
+ ["safari", /Version\/([\d.]+).*Safari/],
+ ];
+ for (const [name, regex] of rules) {
+ const match = ua.match(regex);
+ if (match) {
+ return { browser: name, version: match[1] ?? null };
+ }
+ }
+ } catch {
+ // Ignore errors in non-browser or restricted environments
+ }
+ return { browser: null, version: null };
+}
+
/**
* Transforms the provided HTML event into a log and appends it to the log queue.
* @param {Event} e The event to be logged.
@@ -120,6 +171,7 @@ export function packageLog(
pageUrl: self.location.href,
pageTitle: document.title,
pageReferrer: document.referrer,
+ browser: detectBrowser(),
userAgent: self.navigator.userAgent,
clientTime: timeFields.milli,
microTime: timeFields.micro,
@@ -179,21 +231,14 @@ export function packageCustomLog(
let details = null;
if (typeof detailFcn === "function") {
- if (detailFcn.length === 0) {
- // In the case of a union, the type checker will default to the more stringent
- // type, i.e. the DetailFunction that expects an argument for safety purposes.
- // To avoid this, we must explicitly check the type by asserting it receives
- // no arguments (detailFcn.length === 0) and then cast it to the
- // StaticDetailFunction type.
- const staticDetailFcn = detailFcn as Logging.StaticDetailFunction;
- details = staticDetailFcn();
- }
+ details = (detailFcn as () => Logging.JSONObject | null)();
}
const metaData = {
pageUrl: self.location.href,
pageTitle: document.title,
pageReferrer: document.referrer,
+ browser: detectBrowser(),
userAgent: self.navigator.userAgent,
clientTime: Date.now(),
scrnRes: getScreenRes(),
@@ -279,6 +324,7 @@ export function packageIntervalLog(e: Event) {
pageUrl: self.location.href,
pageTitle: document.title,
pageReferrer: document.referrer,
+ browser: detectBrowser(),
userAgent: self.navigator.userAgent,
count: intervalCounter,
duration: timestamp - intervalTimer, // microseconds
@@ -376,17 +422,14 @@ export function getScreenRes() {
*/
export function getSelector(ele: EventTarget) {
if (ele instanceof HTMLElement || ele instanceof Element) {
- if (ele.localName) {
- return (
- ele.localName +
- (ele.id ? "#" + ele.id : "") +
- (ele.className ? "." + ele.className : "")
- );
- } else if (ele.nodeName) {
+ const rawClass =
+ typeof ele.className === "string"
+ ? ele.className
+ : (ele.getAttribute?.("class") ?? "");
+ const tag = ele.localName || ele.nodeName;
+ if (tag) {
return (
- ele.nodeName +
- (ele.id ? "#" + ele.id : "") +
- (ele.className ? "." + ele.className : "")
+ tag + (ele.id ? "#" + ele.id : "") + (rawClass ? "." + rawClass : "")
);
}
} else if (ele instanceof Document) {
@@ -419,7 +462,6 @@ export function selectorizePath(path: EventTarget[]) {
while ((pathEle = path[i])) {
pathSelectors.push(getSelector(pathEle));
++i;
- pathEle = path[i];
}
return pathSelectors;
}
diff --git a/products/userale/packages/flagon-userale/src/sendLogs.ts b/products/userale/packages/flagon-userale/src/sendLogs.ts
index f62b200..4c5ca31 100644
--- a/products/userale/packages/flagon-userale/src/sendLogs.ts
+++ b/products/userale/packages/flagon-userale/src/sendLogs.ts
@@ -22,12 +22,33 @@ import { Logging } from "@/types";
export { buildHeaders };
let sendIntervalId: string | number | NodeJS.Timeout | undefined;
+let pagehideHandler: (() => void) | null = null;
let activeExporter: LogExporter | null = null;
let activeExporterKey: string | null = null;
let activeCustomExporter: LogExporter | null = null;
+/**
+ * Registers a custom LogExporter instance on the global Configuration singleton.
+ * @param {LogExporter | null} exporter Custom exporter instance or null to reset.
+ * @return {boolean} Whether registration succeeded.
+ */
+export function registerExporter(exporter: LogExporter | null): boolean {
+ if (exporter !== null && typeof exporter?.export !== "function") {
+ return false;
+ }
+ Configuration.getInstance().update({ exporter });
+ activeExporter = null;
+ return true;
+}
+
function resolveExporter(config: Configuration): LogExporter {
- const key = `${config.url}|${config.exporterType ?? "http-json"}`;
+ const key = [
+ config.url,
+ config.otlpEndpoint ?? "",
+ config.exporterFormat ?? "",
+ config.exporterType ?? "http-json",
+ String(Boolean(config.websocketsEnabled)),
+ ].join("|");
if (
activeExporter &&
activeExporterKey === key &&
@@ -70,6 +91,10 @@ export function initSender(logs: Array<Logging.Log>, config: Configuration) {
resolveExporter(config);
sendIntervalId = sendOnInterval(logs, config);
+ if (pagehideHandler && typeof self.removeEventListener === "function") {
+ self.removeEventListener("pagehide", pagehideHandler);
+ pagehideHandler = null;
+ }
sendOnClose(logs, config);
}
@@ -105,7 +130,7 @@ export function sendOnClose(
logs: Array<Logging.Log>,
config: Configuration,
): void {
- self.addEventListener("pagehide", function () {
+ const listener = function () {
if (!config.on) {
return;
}
@@ -118,7 +143,9 @@ export function sendOnClose(
console.error(error);
});
}
- });
+ };
+ pagehideHandler = listener;
+ self.addEventListener("pagehide", listener);
}
/**
diff --git a/products/userale/packages/flagon-userale/src/types.d.ts b/products/userale/packages/flagon-userale/src/types.d.ts
index f5952ee..9ba7f50 100644
--- a/products/userale/packages/flagon-userale/src/types.d.ts
+++ b/products/userale/packages/flagon-userale/src/types.d.ts
@@ -68,6 +68,7 @@ export declare namespace Settings {
browserSessionId: SessionId;
custIndex: CustomIndex;
exporter?: CustomExporter;
+ exporterFormat?: ExporterType;
exporterType?: ExporterType;
headers: Headers;
httpSessionId: SessionId;
diff --git a/products/userale/packages/flagon-userale/src/utils/headers/index.ts b/products/userale/packages/flagon-userale/src/utils/headers/index.ts
index cd23e62..8be8668 100644
--- a/products/userale/packages/flagon-userale/src/utils/headers/index.ts
+++ b/products/userale/packages/flagon-userale/src/utils/headers/index.ts
@@ -66,7 +66,7 @@ export function verifyCallback(callback: Callbacks.HeadersCallback) {
throw new Error("Userale headers callback must be a function");
}
const result = callback();
- if (typeof result !== "object") {
+ if (!result || typeof result !== "object" || Array.isArray(result)) {
throw new Error("Userale headers callback must return an object");
}
for (const [key, value] of Object.entries(result)) {
diff --git a/products/userale/packages/flagon-userale/test/spec/exporters.spec.ts b/products/userale/packages/flagon-userale/test/spec/exporters.spec.ts
index 8ff2c14..6112760 100644
--- a/products/userale/packages/flagon-userale/test/spec/exporters.spec.ts
+++ b/products/userale/packages/flagon-userale/test/spec/exporters.spec.ts
@@ -159,6 +159,21 @@ describe("exporters", () => {
);
jest.useRealTimers();
});
+
+ it("supports exporterFormat aliases ('v2-json', 'otlp-json')", () => {
+ config.update({
+ url: "http://localhost:4318",
+ exporterFormat: "otlp-json",
+ });
+ expect(createExporter(config)).toBeInstanceOf(OtlpJsonExporter);
+
+ config.reset();
+ config.update({
+ url: "http://localhost:8000",
+ exporterFormat: "v2-json",
+ });
+ expect(createExporter(config)).toBeInstanceOf(HttpJsonExporter);
+ });
});
describe("chunkLogsForKeepalive", () => {
diff --git a/products/userale/packages/flagon-userale/test/spec/headers.spec.ts b/products/userale/packages/flagon-userale/test/spec/headers.spec.ts
index 2c00141..e296e45 100644
--- a/products/userale/packages/flagon-userale/test/spec/headers.spec.ts
+++ b/products/userale/packages/flagon-userale/test/spec/headers.spec.ts
@@ -39,9 +39,9 @@ describe("verifyCallback", () => {
it("should throw error for non-function callback", () => {
const nonFunctionCallback = "notAFunction";
- // @ts-ignore
// This is a test case to check if the function throws an error when the callback is not a function
// which should only happen in a Javascript environment
+ // @ts-expect-error testing non-function callback
expect(() => verifyHeadersCallback(nonFunctionCallback)).toThrow(
"Userale headers callback must be a function",
);
@@ -67,6 +67,17 @@ describe("verifyCallback", () => {
const validCallback = jest.fn().mockReturnValue({});
expect(() => verifyHeadersCallback(validCallback)).not.toThrow();
});
+
+ it("should throw error when callback returns null or an array", () => {
+ const nullCallback = jest.fn().mockReturnValue(null);
+ const arrayCallback = jest.fn().mockReturnValue(["a"]);
+ expect(() => verifyHeadersCallback(nullCallback)).toThrow(
+ "Userale headers callback must return an object",
+ );
+ expect(() => verifyHeadersCallback(arrayCallback)).toThrow(
+ "Userale headers callback must return an object",
+ );
+ });
});
describe("registerHeadersCallback", () => {
@@ -85,9 +96,9 @@ describe("registerHeadersCallback", () => {
it("should not register a non-function callback", () => {
const nonFunctionCallback = "notAFunction";
- // @ts-ignore
// This is a test case to check if the function throws an error when the callback is not a function
// which should only happen in a Javascript environment
+ // @ts-expect-error testing non-function callback
expect(registerHeadersCallback(nonFunctionCallback)).toBe(false);
expect(headersCallback).toBeNull();
});
diff --git a/products/userale/packages/flagon-userale/test/spec/packageLogs.spec.ts b/products/userale/packages/flagon-userale/test/spec/packageLogs.spec.ts
index 7c9ea8f..2489c81 100644
--- a/products/userale/packages/flagon-userale/test/spec/packageLogs.spec.ts
+++ b/products/userale/packages/flagon-userale/test/spec/packageLogs.spec.ts
@@ -468,4 +468,11 @@ describe("packageLogs", () => {
"margin-top": "10px",
});
});
+
+ test("getSelector handles SVGElement class attributes without [object SVGAnimatedString]", () => {
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ svg.setAttribute("id", "icon");
+ svg.setAttribute("class", "nav-icon");
+ expect(getSelector(svg)).toBe("svg#icon.nav-icon");
+ });
});
diff --git a/products/userale/test/playwright.config.ts b/products/userale/test/playwright.config.ts
index 3e42e64..1cc0624 100644
--- a/products/userale/test/playwright.config.ts
+++ b/products/userale/test/playwright.config.ts
@@ -19,7 +19,7 @@
import { defineConfig } from "@playwright/test";
-process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS = '1';
+process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS = "1";
/**
* See https://playwright.dev/docs/test-configuration.
@@ -34,7 +34,7 @@ export default defineConfig({
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
- reporter: "html",
+ reporter: [["html", { open: "never" }], ["list"]],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
@@ -77,11 +77,10 @@ export default defineConfig({
],
/* Run your local dev server before starting the tests */
- webServer:
- {
- command: "pnpm run example:run",
- url: "http://127.0.0.1:8000",
- reuseExistingServer: false,
- gracefulShutdown: { signal: 'SIGTERM', timeout: 1000 }
- }
+ webServer: {
+ command: "pnpm run example:run",
+ url: "http://127.0.0.1:8000",
+ reuseExistingServer: false,
+ gracefulShutdown: { signal: "SIGTERM", timeout: 1000 },
+ },
});
diff --git a/products/userale/test/spec/fixtures/extension.fixture.ts b/products/userale/test/spec/fixtures/extension.fixture.ts
index badca35..ec50802 100644
--- a/products/userale/test/spec/fixtures/extension.fixture.ts
+++ b/products/userale/test/spec/fixtures/extension.fixture.ts
@@ -17,40 +17,48 @@
* under the License.
*/
-import { test as base, chromium, type BrowserContext, type Page, type Request } from '@playwright/test';
-import path from 'path';
-import os from 'os';
+import {
+ test as base,
+ chromium,
+ type BrowserContext,
+ type Request,
+} from "@playwright/test";
+import path from "path";
+import os from "os";
export const test = base.extend<{
context: BrowserContext;
extensionId: string;
waitForPostRequest: (
- filterFn?: (req: Request) => boolean
+ filterFn?: (req: Request) => boolean,
) => Promise<Request>;
}>({
- context: async ({ }, use) => {
+ // eslint-disable-next-line no-empty-pattern
+ context: async ({}, use) => {
const pathToExtension = path.join(
__dirname,
- "../../../packages/flagon-userale-ext/build/chrome-mv3-prod/"
+ "../../../packages/flagon-userale-ext/build/chrome-mv3-prod/",
);
const context = await chromium.launchPersistentContext(os.tmpdir(), {
- channel: 'chromium',
+ channel: "chromium",
headless: false,
args: [
`--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}`,
],
});
+ if (context.serviceWorkers().length === 0) {
+ await context.waitForEvent("serviceworker");
+ }
await use(context);
await context.close();
},
extensionId: async ({ context }, use) => {
let [background] = context.serviceWorkers();
- if (!background)
- background = await context.waitForEvent('serviceworker');
+ if (!background) background = await context.waitForEvent("serviceworker");
- const extensionId = background.url().split('/')[2];
+ const extensionId = background.url().split("/")[2];
await use(extensionId);
},
@@ -61,15 +69,16 @@ export const test = base.extend<{
const url = req.url();
const method = req.method();
- if (!url.startsWith('http://localhost:8000') || method !== 'POST') return;
+ if (!url.startsWith("http://localhost:8000") || method !== "POST")
+ return;
if (!filterFn || filterFn(req)) {
- context.off('request', listener); // cleanup
+ context.off("request", listener); // cleanup
resolve(req);
}
};
- context.on('request', listener);
+ context.on("request", listener);
});
};
diff --git a/site/_includes/footer.html b/site/_includes/footer.html
index d8b1e1a..d724976 100644
--- a/site/_includes/footer.html
+++ b/site/_includes/footer.html
@@ -18,18 +18,18 @@
<div class="column">
<h4 class="ui inverted header">Community</h4>
<div class="ui inverted link list">
- <a class="item" href="https://github.com/apache?q=flagon">GitHub</a>
- <a class="item" href="http://stackoverflow.com/questions/tagged/apache-flagon">StackOverflow</a>
+ <a class="item" href="https://github.com/apache/flagon">GitHub</a>
+ <a class="item" href="https://stackoverflow.com/questions/tagged/apache-flagon">StackOverflow</a>
</div>
</div>
<div class="column">
<h4 class="ui inverted header">Apache</h4>
<div class="ui inverted link list">
- <a class="item" href="http://www.apache.org/">Apache Software Foundation</a>
- <a class="item" href="http://www.apache.org/licenses/">License</a>
- <a class="item" href="http://www.apache.org/foundation/sponsorship.html">Sponsorship</a>
- <a class="item" href="http://www.apache.org/foundation/thanks.html">Thanks</a>
- <a class="item" href="http://www.apache.org/security/">Security</a>
+ <a class="item" href="https://www.apache.org/">Apache Software Foundation</a>
+ <a class="item" href="https://www.apache.org/licenses/">License</a>
+ <a class="item" href="https://www.apache.org/foundation/sponsorship.html">Sponsorship</a>
+ <a class="item" href="https://www.apache.org/foundation/thanks.html">Thanks</a>
+ <a class="item" href="https://www.apache.org/security/">Security</a>
<a class="item" href="https://privacy.apache.org/policies/privacy-policy-public.html">Privacy</a>
</div>
</div>
@@ -38,8 +38,8 @@
<div class="ui inverted vertical segment">
<div class="ui two column middle aligned grid">
<div class="center aligned column">
- Copyright © {{ 'now' | date: "%Y" }} The Apache Software Foundation, Licensed under the <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache License, Version 2.0</a>.<br>
- Apache Flagon, Flagon, Apache, the Apache feather logo, the Apache Flagon logo and the Apache Incubator logo are trademarks of The Apache Software Foundation.
+ Copyright © {{ 'now' | date: "%Y" }} The Apache Software Foundation, Licensed under the <a href="https://www.apache.org/licenses/LICENSE-2.0">Apache License, Version 2.0</a>.<br>
+ Apache Flagon, Flagon, Apache, the Apache feather logo, and the Apache Flagon logo are trademarks of The Apache Software Foundation.
</div>
</div>
</div>
diff --git a/site/_includes/useralejsDemo.html b/site/_includes/useralejsDemo.html
index e55b061..e3b2b25 100644
--- a/site/_includes/useralejsDemo.html
+++ b/site/_includes/useralejsDemo.html
@@ -110,8 +110,8 @@
});
var map = L.map('map-example').setView([42.3601, -71.0589], 13);
- L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
- attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
function addLog(log) {
diff --git a/site/_pages/community.html b/site/_pages/community.html
index a4948db..6329f37 100644
--- a/site/_pages/community.html
+++ b/site/_pages/community.html
@@ -46,14 +46,14 @@ title: Community
<div class="content">GitHub</div>
</h3>
<p>
- <a href="https://github.com/apache?q=flagon">Find our code on Github</a> to browse, contribute, and submit issues.
+ <a href="https://github.com/apache/flagon">Find our code on GitHub</a> to browse, contribute, and submit issues.
</p>
<h3 class="ui header">
<i class="stack overflow icon"></i>
<div class="content">StackOverflow</div>
</h3>
<p>
- Get your questions answered on <a href="http://stackoverflow.com/questions/tagged/apache-flagon">StackOverflow</a>.
+ Get your questions answered on <a href="https://stackoverflow.com/questions/tagged/apache-flagon">StackOverflow</a>.
</p>
<div class="ui vertical segment">
diff --git a/site/_pages/releases.html b/site/_pages/releases.html
index 1eaefd6..840747a 100644
--- a/site/_pages/releases.html
+++ b/site/_pages/releases.html
@@ -5,9 +5,9 @@ title: Releases
---
<p xmlns="http://www.w3.org/1999/html">
- Below you can find the official Apache Flagon release distrbution artifacts. Older releases can be found at the <a href="https://downloads.apache.org/flagon">Apache Flagon Archive</a>.
+ Below you can find the official Apache Flagon release distribution artifacts. Older releases can be found at the <a href="https://archive.apache.org/dist/flagon/">Apache Flagon Archive</a>.
- For ongoing developments and future release announcements follow our <a href="https://github.com/apache?q=flagon">source code</a> and sign up for our <a href="../community">mailing lists</a> to keep up to date!
+ For ongoing developments and future release announcements follow our <a href="https://github.com/apache/flagon">source code</a> and sign up for our <a href="../community">mailing lists</a> to keep up to date!
</p>
<p>
@@ -25,7 +25,7 @@ title: Releases
</p>
<p>
- To explore changes in our latest release, please see our <a href="https://github.com/apache/flagon-useralejs/blob/master/CHANGELOG.md">CHANGELOG</a>.
+ To explore changes in our latest release, please see our <a href="https://github.com/apache/flagon/blob/master/products/userale/CHANGELOG.md">CHANGELOG</a>.
Please note, that for convenience, we make UserALE.js available through the NPM <a href="https://www.npmjs.com/package/flagon-userale">'flagon-userale'</a> package.
</p>
@@ -37,7 +37,7 @@ title: Releases
</p>
<p>
- To explore changes in our latest release, please see our <a href="https://github.com/apache/flagon-distill/blob/master/CHANGELOG.md">CHANGELOG</a>.
+ To explore changes in our latest release, please see our <a href="https://github.com/apache/flagon/blob/master/products/distill/CHANGELOG.rst">CHANGELOG</a>.
Please note, that for convenience, we make Distill available through the PyPi <a href="https://pypi.org/project/flagon-distill/">'flagon-distill'</a> package.
</p>
diff --git a/site/semantic/dist/components/site.css b/site/semantic/dist/components/site.css
index 028adda..5fb82da 100755
--- a/site/semantic/dist/components/site.css
+++ b/site/semantic/dist/components/site.css
@@ -13,7 +13,6 @@
Page
*******************************/
-@import url('https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic&subset=latin');
html,
body {
height: 100%;
diff --git a/site/semantic/dist/components/site.min.css b/site/semantic/dist/components/site.min.css
index fe2e7a0..61ebe93 100755
--- a/site/semantic/dist/components/site.min.css
+++ b/site/semantic/dist/components/site.min.css
@@ -6,4 +6,4 @@
* Released under the MIT license
* http://opensource.org/licenses/MIT
*
- */@import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic&subset=latin);body,html{height:100%}html{font-size:14px}body{margin:0;padding:0;overflow-x:hidden;min-width:320px;background:#EBEBEB;font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;font-size:16px;line-height:1.4285em;color:#262626;font-smoothing:antialiased}h1,h2,h3,h4,h5{font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;line-height:1.2857em;margin:calc(2rem - .14285em) 0 1rem; [...]
\ No newline at end of file
+ */body,html{height:100%}html{font-size:14px}body{margin:0;padding:0;overflow-x:hidden;min-width:320px;background:#EBEBEB;font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;font-size:16px;line-height:1.4285em;color:#262626;font-smoothing:antialiased}h1,h2,h3,h4,h5{font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;line-height:1.2857em;margin:calc(2rem - .14285em) 0 1rem;font-weight:700;padding:0}h1{min-height:1rem;font-size:2rem}h2{font-size:1.714rem}h3{font-size:1.28 [...]
\ No newline at end of file
diff --git a/site/semantic/dist/semantic.css b/site/semantic/dist/semantic.css
index 026f8e9..3046f7b 100644
--- a/site/semantic/dist/semantic.css
+++ b/site/semantic/dist/semantic.css
@@ -7,9 +7,7 @@
* Released under the MIT license
* http://opensource.org/licenses/MIT
*
- */
-@import url('https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic&subset=latin');
-/*!
+ /*!
* # Semantic UI 2.2.6 - Reset
* http://github.com/semantic-org/semantic-ui/
*
diff --git a/site/semantic/dist/semantic.min.css b/site/semantic/dist/semantic.min.css
index 0068bfb..9bae84e 100644
--- a/site/semantic/dist/semantic.min.css
+++ b/site/semantic/dist/semantic.min.css
@@ -8,4 +8,4 @@
* http://opensource.org/licenses/MIT
*
*/
-@import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic&subset=latin);*,:after,:before{box-sizing:inherit}html{box-sizing:border-box;font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}input[type=text],input[type=email],input[type=search],input[type=password]{-webkit-appearance:none;-moz-appearance:none}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{dis [...]
\ No newline at end of file
+*,:after,:before{box-sizing:inherit}html{box-sizing:border-box;font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}input[type=text],input[type=email],input[type=search],input[type=password]{-webkit-appearance:none;-moz-appearance:none}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],temp [...]
\ No newline at end of file