- ) : root ? (
+ ) : version ? (
) : null}
diff --git a/ecosystem-explorer/src/features/java-agent/java-instrumentation-list-page.tsx b/ecosystem-explorer/src/features/java-agent/java-instrumentation-list-page.tsx
index 9e4e72e2..91a29305 100644
--- a/ecosystem-explorer/src/features/java-agent/java-instrumentation-list-page.tsx
+++ b/ecosystem-explorer/src/features/java-agent/java-instrumentation-list-page.tsx
@@ -21,8 +21,7 @@ import {
InstrumentationFilterBar,
} from "@/features/java-agent/components/instrumentation-filter-bar.tsx";
import { useMemo, useState, useEffect } from "react";
-import { useParams, useNavigate, useSearchParams } from "react-router-dom";
-import { X } from "lucide-react";
+import { useParams, useNavigate } from "react-router-dom";
import { InstrumentationGroupCard } from "@/features/java-agent/components/instrumentation-group-card.tsx";
import { VersionSelector } from "@/features/java-agent/components/version-selector";
import { getInstrumentationDisplayName } from "./utils/format";
@@ -37,28 +36,14 @@ export function JavaInstrumentationListPage() {
const latestVersion = versionsData?.versions.find((v) => v.is_latest)?.version ?? "";
- const [searchParams] = useSearchParams();
- const invalidVersion = searchParams.get("redirectedFrom");
- const [bannerDismissed, setBannerDismissed] = useState(false);
-
- const isVersionValid =
- !versionParam ||
- versionParam === "latest" ||
- (!!versionsData && versionsData.versions.some((v) => v.version === versionParam));
-
// Redirect /java-agent/instrumentation (no version) or /latest to the actual latest version
- // Also redirect invalid versions to latest and show a dismissible inline alert
useEffect(() => {
if (versionsData && latestVersion) {
if (!versionParam || versionParam === "latest") {
navigate(`/java-agent/instrumentation/${latestVersion}`, { replace: true });
- } else if (!isVersionValid) {
- navigate(`/java-agent/instrumentation/${latestVersion}?redirectedFrom=${versionParam}`, {
- replace: true,
- });
}
}
- }, [versionParam, versionsData, latestVersion, navigate, isVersionValid]);
+ }, [versionParam, versionsData, latestVersion, navigate]);
const resolvedVersion = versionParam && versionParam !== "latest" ? versionParam : "";
@@ -161,22 +146,6 @@ export function JavaInstrumentationListPage() {
- {invalidVersion && !bannerDismissed && (
-
-
- Version "{invalidVersion}" was not found. Showing the latest version (
- {latestVersion}) instead.
-
-
-
- )}
-
diff --git a/ecosystem-explorer/src/hooks/use-configuration-builder.test.ts b/ecosystem-explorer/src/hooks/use-configuration-builder.test.ts
index d170c6fb..5963d4d3 100644
--- a/ecosystem-explorer/src/hooks/use-configuration-builder.test.ts
+++ b/ecosystem-explorer/src/hooks/use-configuration-builder.test.ts
@@ -24,7 +24,7 @@ import type {
TextInputNode,
} from "@/types/configuration";
-const STORAGE_KEY = "otel-config-builder-state-v3";
+const STORAGE_KEY = "otel-config-builder-state-v2";
const mockSchema: GroupNode = {
controlType: "group",
@@ -283,24 +283,6 @@ describe("useConfigurationBuilderState", () => {
const { result } = renderHook(() => useConfigurationBuilderState(mockSchema, "1.0.0", null));
expect(result.current.state.values).toEqual({});
});
-
- it("ignores state stored under the previous v2 storage key", () => {
- localStorage.setItem(
- "otel-config-builder-state-v2",
- JSON.stringify({
- schemaVersion: "1.0.0",
- state: {
- version: "1.0.0",
- values: { file_format: "from-v2-storage" },
- enabledSections: {},
- validationErrors: {},
- isDirty: true,
- },
- })
- );
- const { result } = renderHook(() => useConfigurationBuilderState(mockSchema, "1.0.0", null));
- expect(result.current.state.values).toEqual({});
- });
});
it("resetToDefaults with starter hydrates from starter", () => {
diff --git a/ecosystem-explorer/src/hooks/use-configuration-builder.ts b/ecosystem-explorer/src/hooks/use-configuration-builder.ts
index 8628ade9..7c2b1f09 100644
--- a/ecosystem-explorer/src/hooks/use-configuration-builder.ts
+++ b/ecosystem-explorer/src/hooks/use-configuration-builder.ts
@@ -53,7 +53,7 @@ import {
validateAll as validateAllNodes,
} from "@/lib/config-validation";
-const STORAGE_KEY = "otel-config-builder-state-v3";
+const STORAGE_KEY = "otel-config-builder-state-v2";
export interface ConfigurationBuilderStateContextValue {
state: ConfigurationBuilderState;
diff --git a/ecosystem-explorer/src/lib/api/configuration-data.test.ts b/ecosystem-explorer/src/lib/api/configuration-data.test.ts
index 42a64769..9d435a7b 100644
--- a/ecosystem-explorer/src/lib/api/configuration-data.test.ts
+++ b/ecosystem-explorer/src/lib/api/configuration-data.test.ts
@@ -133,7 +133,7 @@ describe("configuration-data", () => {
});
await expect(configData.loadConfigSchema("1.0.0")).rejects.toThrow(
- "Failed to load config-schema-1.0.0 from /data/configuration/versions/1.0.0.json: 500 Internal Server Error"
+ "Failed to load config-schema-1.0.0: 500 Internal Server Error"
);
});
});
diff --git a/ecosystem-explorer/src/lib/api/fetch-with-cache.test.ts b/ecosystem-explorer/src/lib/api/fetch-with-cache.test.ts
index c638ce7d..bde11374 100644
--- a/ecosystem-explorer/src/lib/api/fetch-with-cache.test.ts
+++ b/ecosystem-explorer/src/lib/api/fetch-with-cache.test.ts
@@ -72,7 +72,7 @@ describe("fetchWithCache", () => {
});
await expect(fetchWithCache("key", "/url", idbCache.STORES.METADATA)).rejects.toThrow(
- "Failed to load key from /url: 404 Not Found"
+ "Failed to load key: 404 Not Found"
);
});
@@ -206,22 +206,4 @@ describe("fetchWithCache", () => {
expect(global.fetch).toHaveBeenCalledWith("/url");
});
});
-
- it("serves stale cache when response is 200 but content-type is text/html", async () => {
- const staleData = { test: "stale-html-fallback" };
- vi.spyOn(idbCache, "getCached").mockImplementation(async (_key, _store, options) => {
- if (options?.allowExpired) return staleData;
- return null;
- });
-
- globalThis.fetch = vi.fn().mockResolvedValue(
- new Response("", {
- status: 200,
- headers: { "content-type": "text/html" },
- })
- ) as unknown as typeof fetch;
-
- const result = await fetchWithCache("test-html", "/data.json", STORES.CONFIGURATION);
- expect(result).toEqual(staleData);
- });
});
diff --git a/ecosystem-explorer/src/lib/api/fetch-with-cache.ts b/ecosystem-explorer/src/lib/api/fetch-with-cache.ts
index 96d570c9..efcec54e 100644
--- a/ecosystem-explorer/src/lib/api/fetch-with-cache.ts
+++ b/ecosystem-explorer/src/lib/api/fetch-with-cache.ts
@@ -17,60 +17,8 @@ import { getCached, setCached, isIDBAvailable, type StoreName } from "./idb-cach
const inflightRequests = new Map>();
-/**
- * Validates a path segment to prevent path traversal attacks.
- * Only allows alphanumeric characters, dots, underscores, and hyphens.
- */
-export function validatePathSegment(segment: string): void {
- if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(segment) || segment.includes("..")) {
- throw new Error(`Invalid path segment: ${segment}`);
- }
-}
-
-/**
- * Resolves a data path by combining BASE_URL with path segments.
- * Centralizes security validation and path construction.
- */
-export function resolveDataPath(base: string, ...segments: string[]): string {
- const baseUrl = import.meta.env.BASE_URL || "";
- const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
- const parts = [normalizedBase, base.startsWith("/") ? base.slice(1) : base];
- for (const segment of segments) {
- validatePathSegment(segment);
- parts.push(segment);
- }
- return parts.join("/");
-}
-
-const DEFAULT_MAX_RETRIES = 3;
-const DEFAULT_RETRY_DELAY_MS = 1000;
-
-async function fetchWithRetry(
- url: string,
- retries = DEFAULT_MAX_RETRIES,
- delayMs = DEFAULT_RETRY_DELAY_MS
-): Promise {
- const maxAttempts = Math.max(1, retries);
- let lastError: unknown;
- for (let i = 0; i < maxAttempts; i++) {
- try {
- // HTTP responses (ok or non-ok) are returned immediately - no retry.
- // Only real network failures (catch block) trigger retries.
- return await fetch(url);
- } catch (error) {
- lastError = error;
- if (i === maxAttempts - 1) throw error;
- }
- await new Promise((resolve) => setTimeout(resolve, delayMs * Math.pow(2, i)));
- }
- throw lastError;
-}
-
export interface FetchWithCacheOptions {
allow404?: boolean;
- format?: "json" | "text";
- retryCount?: number;
- retryDelayMs?: number;
/**
* Optional validator for cached data. When provided, cached data that fails
* validation is ignored for the current request and a fresh network request
@@ -95,79 +43,34 @@ export async function fetchWithCache(
if (isIDBAvailable()) {
const cachedData = await getCached(cacheKey, storeType);
if (cachedData !== null) {
- if (!options?.validate) return cachedData;
+ if (!options?.validate) {
+ return cachedData;
+ }
try {
- if (options.validate(cachedData)) return cachedData;
+ if (options.validate(cachedData)) {
+ return cachedData;
+ }
} catch {
- // validator exception = treat as invalid
- }
- }
- }
-
- let response: Response;
- try {
- response = await fetchWithRetry(url, options?.retryCount, options?.retryDelayMs);
- } catch (error) {
- if (isIDBAvailable()) {
- const staleData = await getCached(cacheKey, storeType, { allowExpired: true });
- if (staleData !== null) {
- if (options?.validate && !options.validate(staleData)) throw error;
- console.warn("Network error, serving stale cache:", cacheKey, error);
- return staleData;
+ // treat validator exceptions as validation failures and fall back to network fetch
}
}
- throw error;
}
+ const response = await fetch(url);
if (!response.ok) {
- if (response.status === 404 && options?.allow404) return null;
-
- if (isIDBAvailable()) {
- const staleData = await getCached(cacheKey, storeType, { allowExpired: true });
- if (staleData !== null) {
- if (options?.validate && !options.validate(staleData)) {
- throw new Error(
- `Failed to load ${cacheKey}: ${response.status} ${response.statusText}`
- );
- }
- console.warn("HTTP error, serving stale cache:", { cacheKey, status: response.status });
- return staleData;
- }
+ if (response.status === 404 && options?.allow404) {
+ return null;
}
- throw new Error(
- `Failed to load ${cacheKey} from ${url}: ${response.status} ${response.statusText}`
- );
+ throw new Error(`Failed to load ${cacheKey}: ${response.status} ${response.statusText}`);
}
- // SPA 200 fallback: CDNs can return 200 + HTML during deployment propagation.
- // Use optional chaining so test mocks without headers don't crash.
- const contentType = response.headers?.get?.("content-type") ?? "";
- if (contentType.includes("text/html")) {
- if (isIDBAvailable()) {
- const staleData = await getCached(cacheKey, storeType, { allowExpired: true });
- if (staleData !== null) {
- if (options?.validate && !options.validate(staleData)) {
- throw new Error(
- `Failed to load ${cacheKey}: unexpected content-type "${contentType}"`
- );
- }
- console.warn("CDN returned non-JSON 200, serving stale cache:", {
- cacheKey,
- contentType,
- });
- return staleData;
- }
- }
- throw new Error(`Failed to load ${cacheKey}: unexpected content-type "${contentType}"`);
- }
+ const data = await response.json();
- const format = options?.format ?? "json";
- const data = format === "json" ? await response.json() : await response.text();
if (isIDBAvailable()) {
try {
await setCached(cacheKey, data, storeType);
} catch {
- // Cache write failures must not block data loading
+ // Cache write failure should not break data loading
}
}
diff --git a/ecosystem-explorer/src/lib/api/idb-cache.test.ts b/ecosystem-explorer/src/lib/api/idb-cache.test.ts
index 4fde05a0..9d9f169a 100644
--- a/ecosystem-explorer/src/lib/api/idb-cache.test.ts
+++ b/ecosystem-explorer/src/lib/api/idb-cache.test.ts
@@ -15,15 +15,7 @@
*/
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import "fake-indexeddb/auto";
-import {
- initDB,
- getCached,
- setCached,
- clearAllCached,
- closeDB,
- pruneOldEntries,
- STORES,
-} from "./idb-cache";
+import { initDB, getCached, setCached, clearAllCached, closeDB, STORES } from "./idb-cache";
describe("idb-cache", () => {
beforeEach(async () => {
@@ -108,7 +100,7 @@ describe("idb-cache", () => {
expect(result).toEqual(data);
});
- it("should return null for entries older than 24 hours but keep them for stale fallback", async () => {
+ it("should return null and delete entries older than 24 hours", async () => {
const key = "stale-key";
const data = { value: "stale" };
const now = Date.now();
@@ -118,23 +110,10 @@ describe("idb-cache", () => {
const result = await getCached(key, STORES.METADATA);
expect(result).toBeNull();
- // Entry must be KEPT (not deleted) to support stale-cache fallback
+ // Confirm it was deleted from the store
const db = await initDB();
const raw = await db.get(STORES.METADATA, key);
- expect(raw).toBeDefined();
- expect(raw.data).toEqual(data);
- });
-
- it("should return stale data when allowExpired is true", async () => {
- const key = "stale-key";
- const data = { value: "stale" };
- const now = Date.now();
-
- await setCached(key, data, STORES.METADATA);
- vi.setSystemTime(now + 25 * 60 * 60 * 1000);
- const result = await getCached(key, STORES.METADATA, { allowExpired: true });
-
- expect(result).toEqual(data);
+ expect(raw).toBeUndefined();
});
it("should return null for non-existent keys", async () => {
@@ -191,48 +170,4 @@ describe("idb-cache", () => {
expect(result).toEqual({ data: "value" });
});
});
-
- describe("pruneOldEntries", () => {
- it("should remove entries not accessed within the threshold", async () => {
- const now = Date.now();
-
- await setCached("new-key", { data: "new" }, STORES.METADATA);
-
- vi.setSystemTime(now - 10 * 24 * 60 * 60 * 1000); // 10 days ago
- await setCached("stale-key", { data: "stale" }, STORES.METADATA);
-
- vi.setSystemTime(now);
- await pruneOldEntries(7);
-
- const newResult = await getCached("new-key", STORES.METADATA);
- expect(newResult).not.toBeNull();
-
- const db = await initDB();
- expect(await db.get(STORES.METADATA, "stale-key")).toBeUndefined();
- });
-
- it("should NOT prune more than once per 24 hours", async () => {
- const startTime = Date.now();
-
- vi.setSystemTime(startTime - 10 * 24 * 60 * 60 * 1000);
- await setCached("old-1", { d: 1 }, STORES.METADATA);
- vi.setSystemTime(startTime);
- await pruneOldEntries(7);
-
- const db = await initDB();
- expect(await db.get(STORES.METADATA, "old-1")).toBeUndefined();
-
- // Immediately after: guard should block second prune
- vi.setSystemTime(startTime - 10 * 24 * 60 * 60 * 1000);
- await setCached("old-2", { d: 2 }, STORES.METADATA);
- vi.setSystemTime(startTime + 1000);
- await pruneOldEntries(7);
- expect(await db.get(STORES.METADATA, "old-2")).toBeDefined();
-
- // 25 hours later: guard expires, prune runs again
- vi.setSystemTime(startTime + 25 * 60 * 60 * 1000);
- await pruneOldEntries(7);
- expect(await db.get(STORES.METADATA, "old-2")).toBeUndefined();
- });
- });
});
diff --git a/ecosystem-explorer/src/lib/api/idb-cache.ts b/ecosystem-explorer/src/lib/api/idb-cache.ts
index a6646ec2..0a5d8b4f 100644
--- a/ecosystem-explorer/src/lib/api/idb-cache.ts
+++ b/ecosystem-explorer/src/lib/api/idb-cache.ts
@@ -17,9 +17,7 @@ import { openDB, type IDBPDatabase } from "idb";
const DB_NAME = "otel-explorer-cache";
const DB_VERSION = 8;
-const CACHE_EXPIRATION_MS = 24 * 60 * 60 * 1000;
-const PRUNE_INTERVAL_MS = 24 * 60 * 60 * 1000;
-const PRUNE_KEY = "__internal_last_pruned_at";
+const CACHE_EXPIRATION_MS = 24 * 60 * 60 * 1000; // 24 hours
export const STORES = {
METADATA: "metadata",
@@ -34,7 +32,6 @@ interface CacheEntry {
key: string;
data: T;
cachedAt: number;
- lastAccessedAt?: number;
}
let dbInstance: IDBPDatabase | null = null;
@@ -43,7 +40,9 @@ let dbInitFailed = false;
function isExpired(cachedAt: number): boolean {
const now = Date.now();
- if (cachedAt > now) return true;
+ if (cachedAt > now) {
+ return true;
+ }
return now - cachedAt > CACHE_EXPIRATION_MS;
}
@@ -51,24 +50,33 @@ export async function initDB(): Promise {
if (!isIDBAvailable()) {
throw new Error("IndexedDB is not available in this environment");
}
+
if (dbInitFailed) {
throw new Error("IndexedDB initialization previously failed");
}
- if (dbInstance) return dbInstance;
- if (dbInitPromise) return dbInitPromise;
+
+ if (dbInstance) {
+ return dbInstance;
+ }
+
+ if (dbInitPromise) {
+ return dbInitPromise;
+ }
dbInitPromise = (async () => {
try {
const db = await openDB(DB_NAME, DB_VERSION, {
upgrade(db) {
- for (const storeName of Object.values(STORES)) {
+ const stores = Object.values(STORES);
+ stores.forEach((storeName) => {
if (db.objectStoreNames.contains(storeName)) {
db.deleteObjectStore(storeName);
}
db.createObjectStore(storeName, { keyPath: "key" });
- }
+ });
},
});
+
dbInstance = db;
dbInitPromise = null;
return db;
@@ -83,36 +91,21 @@ export async function initDB(): Promise {
return dbInitPromise;
}
-export async function getCached(
- key: string,
- store: StoreName,
- options?: { allowExpired?: boolean }
-): Promise {
+export async function getCached(key: string, store: StoreName): Promise {
try {
const db = await initDB();
const entry = await db.get(store, key);
- if (!entry) return null;
+
+ if (!entry) {
+ return null;
+ }
const cacheEntry = entry as CacheEntry;
if (isExpired(cacheEntry.cachedAt)) {
- if (options?.allowExpired) {
- const now = Date.now();
- const lastAccessed = cacheEntry.lastAccessedAt ?? 0;
- if (now - lastAccessed > 60 * 60 * 1000) {
- cacheEntry.lastAccessedAt = now;
- db.put(store, cacheEntry).catch(() => {});
- }
- return cacheEntry.data;
- }
+ await db.delete(store, key);
return null;
}
- const now = Date.now();
- const lastAccessed = cacheEntry.lastAccessedAt ?? 0;
- if (now - lastAccessed > 60 * 60 * 1000) {
- cacheEntry.lastAccessedAt = now;
- db.put(store, cacheEntry).catch(() => {});
- }
return cacheEntry.data;
} catch (error) {
console.error(`Failed to get cached data for %s:`, key, error);
@@ -123,12 +116,13 @@ export async function getCached(
export async function setCached(key: string, data: T, store: StoreName): Promise {
try {
const db = await initDB();
+
const entry: CacheEntry = {
key,
data,
cachedAt: Date.now(),
- lastAccessedAt: Date.now(),
};
+
await db.put(store, entry);
} catch (error) {
console.error(`Failed to cache data for %s:`, key, error);
@@ -138,60 +132,13 @@ export async function setCached(key: string, data: T, store: StoreName): Prom
export async function clearAllCached(): Promise {
try {
const db = await initDB();
- await Promise.all(Object.values(STORES).map((store) => db.clear(store)));
+ const stores = Object.values(STORES);
+ await Promise.all(stores.map((store) => db.clear(store)));
} catch (error) {
console.error("Failed to clear cache:", error);
}
}
-/**
- * Removes entries not accessed within `maxAgeDays` days.
- * A 24-hour frequency guard prevents excessive disk I/O on every navigation.
- */
-export async function pruneOldEntries(maxAgeDays = 7): Promise {
- try {
- const db = await initDB();
- const now = Date.now();
-
- const lastPruneEntry = await db.get(STORES.METADATA, PRUNE_KEY);
- if (lastPruneEntry) {
- const lastPrunedAt = (lastPruneEntry as CacheEntry).data;
- if (now - lastPrunedAt < PRUNE_INTERVAL_MS) return;
- }
-
- const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
-
- for (const store of Object.values(STORES)) {
- const tx = db.transaction(store, "readwrite");
- let cursor = await tx.store.openCursor();
-
- while (cursor) {
- const entry = cursor.value as CacheEntry;
- if (entry.key === PRUNE_KEY) {
- cursor = await cursor.continue();
- continue;
- }
- // Coalesce: prefer lastAccessedAt, fall back to cachedAt for older entries
- const lastAccessed = entry.lastAccessedAt ?? entry.cachedAt;
- if (now - lastAccessed > maxAgeMs) {
- await cursor.delete();
- }
- cursor = await cursor.continue();
- }
- await tx.done;
- }
-
- await db.put(STORES.METADATA, {
- key: PRUNE_KEY,
- data: now,
- cachedAt: now,
- lastAccessedAt: now,
- });
- } catch (error) {
- console.error("Failed to prune old cache entries:", error);
- }
-}
-
export function closeDB(): void {
if (dbInstance) {
dbInstance.close();
diff --git a/ecosystem-explorer/src/lib/api/javaagent-data.test.ts b/ecosystem-explorer/src/lib/api/javaagent-data.test.ts
index ba15a9d3..e23c95cc 100644
--- a/ecosystem-explorer/src/lib/api/javaagent-data.test.ts
+++ b/ecosystem-explorer/src/lib/api/javaagent-data.test.ts
@@ -47,10 +47,9 @@ describe("javaagent-data", () => {
},
};
- beforeEach(async () => {
+ beforeEach(() => {
vi.resetAllMocks();
global.fetch = vi.fn();
- await idbCache.clearAllCached();
idbCache.closeDB();
});
@@ -103,7 +102,7 @@ describe("javaagent-data", () => {
});
await expect(javaagentData.loadVersions()).rejects.toThrow(
- /Failed to load versions-index from .*: 404 Not Found/
+ "Failed to load versions-index: 404 Not Found"
);
});
@@ -150,8 +149,9 @@ describe("javaagent-data", () => {
const request2 = javaagentData.loadVersions();
const request3 = javaagentData.loadVersions();
- await Promise.resolve(); // Allow microtasks to run
- expect(global.fetch).toHaveBeenCalledTimes(1);
+ await vi.waitFor(() => {
+ expect(global.fetch).toHaveBeenCalledTimes(1);
+ });
fetchResolve!({
ok: true,
@@ -317,60 +317,4 @@ describe("javaagent-data", () => {
expect(result).toEqual([]);
});
});
-
- describe("loadLibraryReadme", () => {
- it("should load library README markdown", async () => {
- const content = "# My Library README";
- (global.fetch as ReturnType).mockResolvedValue({
- ok: true,
- text: async () => content,
- });
-
- const result = await javaagentData.loadLibraryReadme("mylib", "abc123def456");
-
- expect(result).toBe(content);
- expect(global.fetch).toHaveBeenCalledWith(
- expect.stringContaining("/markdown/mylib-abc123def456.md")
- );
- });
-
- it("should propagate fetch errors when loading README", async () => {
- (global.fetch as ReturnType).mockResolvedValue({
- ok: false,
- status: 404,
- statusText: "Not Found",
- });
-
- await expect(javaagentData.loadLibraryReadme("mylib", "abc123def456")).rejects.toThrow(
- /Failed to load readme-mylib-abc123def456 from.*: 404 Not Found/
- );
- });
- });
-
- describe("loadGlobalConfigurations", () => {
- it("should load global configurations", async () => {
- const config = { some: "config" };
- (global.fetch as ReturnType).mockResolvedValue({
- ok: true,
- json: async () => config,
- });
-
- const result = await javaagentData.loadGlobalConfigurations();
-
- expect(result).toEqual(config);
- expect(global.fetch).toHaveBeenCalledWith("/data/javaagent/global-configurations.json");
- });
-
- it("should throw error when global configurations fetch fails", async () => {
- (global.fetch as ReturnType).mockResolvedValue({
- ok: false,
- status: 500,
- statusText: "Server Error",
- });
-
- await expect(javaagentData.loadGlobalConfigurations()).rejects.toThrow(
- /Failed to load global-configurations from .*: 500 Server Error/
- );
- });
- });
});
diff --git a/ecosystem-explorer/src/lib/api/javaagent-data.ts b/ecosystem-explorer/src/lib/api/javaagent-data.ts
index ac48041b..40692f7f 100644
--- a/ecosystem-explorer/src/lib/api/javaagent-data.ts
+++ b/ecosystem-explorer/src/lib/api/javaagent-data.ts
@@ -13,41 +13,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import type {
- InstrumentationData,
- VersionManifest,
- VersionsIndex,
- Configuration,
-} from "@/types/javaagent";
-import { STORES, pruneOldEntries } from "./idb-cache";
-import { fetchWithCache, resolveDataPath } from "./fetch-with-cache";
+import type { InstrumentationData, VersionManifest, VersionsIndex } from "@/types/javaagent";
+import { STORES } from "./idb-cache";
+import { fetchWithCache } from "./fetch-with-cache";
-const BASE_DIR = "data/javaagent";
-
-export interface GlobalConfiguration extends Configuration {
- instrumentations?: string[];
-}
+const BASE_PATH = "/data/javaagent";
export async function loadVersions(): Promise {
const data = await fetchWithCache(
"versions-index",
- resolveDataPath(BASE_DIR, "versions-index.json"),
+ `${BASE_PATH}/versions-index.json`,
STORES.METADATA,
{ validate: (d) => Array.isArray(d.versions) && d.versions.length > 0 }
);
if (!data) throw new Error("Versions index returned null unexpectedly");
-
- // Trigger background cache pruning. The guard inside pruneOldEntries ensures
- // this runs at most once every 24 hours regardless of how often loadVersions is called.
- pruneOldEntries().catch(() => {});
-
return data;
}
export async function loadVersionManifest(version: string): Promise {
const data = await fetchWithCache(
`manifest-${version}`,
- resolveDataPath(BASE_DIR, "versions", `${version}-index.json`),
+ `${BASE_PATH}/versions/${version}-index.json`,
STORES.METADATA,
{
validate: (d) =>
@@ -80,7 +66,7 @@ export async function loadInstrumentation(
const filename = `${id}-${hash}.json`;
const data = await fetchWithCache(
`instrumentation-${hash}`,
- resolveDataPath(BASE_DIR, "instrumentations", id, filename),
+ `${BASE_PATH}/instrumentations/${id}/${filename}`,
STORES.INSTRUMENTATIONS
);
if (!data) throw new Error(`Instrumentation "${id}" returned null unexpectedly`);
@@ -102,29 +88,10 @@ export async function loadAllInstrumentations(version: string): Promise {
- const baseUrl = import.meta.env.BASE_URL || "";
- const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
- const url = `${normalizedBase}/${BASE_DIR}/markdown/${libraryName}-${markdownHash}.md`;
- const data = await fetchWithCache(
- `readme-${libraryName}-${markdownHash}`,
- url,
- STORES.METADATA,
- { format: "text" }
- );
- if (data === null) {
- throw new Error(`README for ${libraryName} returned null unexpectedly`);
- }
- return data;
-}
-
-export async function loadGlobalConfigurations(): Promise {
- const data = await fetchWithCache(
+export async function loadGlobalConfigurations() {
+ const data = await fetchWithCache(
"global-configurations",
- resolveDataPath(BASE_DIR, "global-configurations.json"),
+ `${BASE_PATH}/global-configurations.json`,
STORES.GLOBAL_CONFIGURATIONS
);
if (!data) throw new Error("Global configurations returned null unexpectedly");
diff --git a/ecosystem-explorer/src/lib/feature-flags.test.ts b/ecosystem-explorer/src/lib/feature-flags.test.ts
index 506694e3..e2d0a64d 100644
--- a/ecosystem-explorer/src/lib/feature-flags.test.ts
+++ b/ecosystem-explorer/src/lib/feature-flags.test.ts
@@ -22,42 +22,42 @@ describe("isEnabled", () => {
});
it("should return true for 'true'", () => {
- vi.stubEnv("VITE_FEATURE_FLAG_COLLECTOR_PAGE", "true");
- expect(isEnabled("COLLECTOR_PAGE")).toBe(true);
+ vi.stubEnv("VITE_FEATURE_FLAG_JAVA_CONFIG_BUILDER", "true");
+ expect(isEnabled("JAVA_CONFIG_BUILDER")).toBe(true);
});
it("should return true for '1'", () => {
- vi.stubEnv("VITE_FEATURE_FLAG_COLLECTOR_PAGE", "1");
- expect(isEnabled("COLLECTOR_PAGE")).toBe(true);
+ vi.stubEnv("VITE_FEATURE_FLAG_JAVA_CONFIG_BUILDER", "1");
+ expect(isEnabled("JAVA_CONFIG_BUILDER")).toBe(true);
});
it("should return true for 'yes'", () => {
- vi.stubEnv("VITE_FEATURE_FLAG_COLLECTOR_PAGE", "yes");
- expect(isEnabled("COLLECTOR_PAGE")).toBe(true);
+ vi.stubEnv("VITE_FEATURE_FLAG_JAVA_CONFIG_BUILDER", "yes");
+ expect(isEnabled("JAVA_CONFIG_BUILDER")).toBe(true);
});
it("should return true for uppercase truthy values", () => {
- vi.stubEnv("VITE_FEATURE_FLAG_COLLECTOR_PAGE", "TRUE");
- expect(isEnabled("COLLECTOR_PAGE")).toBe(true);
+ vi.stubEnv("VITE_FEATURE_FLAG_JAVA_CONFIG_BUILDER", "TRUE");
+ expect(isEnabled("JAVA_CONFIG_BUILDER")).toBe(true);
});
it("should return false for 'false'", () => {
- vi.stubEnv("VITE_FEATURE_FLAG_COLLECTOR_PAGE", "false");
- expect(isEnabled("COLLECTOR_PAGE")).toBe(false);
+ vi.stubEnv("VITE_FEATURE_FLAG_JAVA_CONFIG_BUILDER", "false");
+ expect(isEnabled("JAVA_CONFIG_BUILDER")).toBe(false);
});
it("should return false for '0'", () => {
- vi.stubEnv("VITE_FEATURE_FLAG_COLLECTOR_PAGE", "0");
- expect(isEnabled("COLLECTOR_PAGE")).toBe(false);
+ vi.stubEnv("VITE_FEATURE_FLAG_JAVA_CONFIG_BUILDER", "0");
+ expect(isEnabled("JAVA_CONFIG_BUILDER")).toBe(false);
});
it("should return false for 'no'", () => {
- vi.stubEnv("VITE_FEATURE_FLAG_COLLECTOR_PAGE", "no");
- expect(isEnabled("COLLECTOR_PAGE")).toBe(false);
+ vi.stubEnv("VITE_FEATURE_FLAG_JAVA_CONFIG_BUILDER", "no");
+ expect(isEnabled("JAVA_CONFIG_BUILDER")).toBe(false);
});
it("should return false for an empty string", () => {
- vi.stubEnv("VITE_FEATURE_FLAG_COLLECTOR_PAGE", "");
- expect(isEnabled("COLLECTOR_PAGE")).toBe(false);
+ vi.stubEnv("VITE_FEATURE_FLAG_JAVA_CONFIG_BUILDER", "");
+ expect(isEnabled("JAVA_CONFIG_BUILDER")).toBe(false);
});
});
diff --git a/ecosystem-explorer/src/lib/feature-flags.ts b/ecosystem-explorer/src/lib/feature-flags.ts
index d6f0f6e0..f3bc5fab 100644
--- a/ecosystem-explorer/src/lib/feature-flags.ts
+++ b/ecosystem-explorer/src/lib/feature-flags.ts
@@ -18,6 +18,9 @@
// There is no need to edit anything else in this file.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const FEATURE_FLAGS = [
+ // Declarative Configuration Builder for Java - Still in development
+ "JAVA_CONFIG_BUILDER",
+
// Collector Page - Still in development
"COLLECTOR_PAGE",
diff --git a/ecosystem-explorer/src/lib/schema-defaults.test.ts b/ecosystem-explorer/src/lib/schema-defaults.test.ts
index f6d339e3..d8136187 100644
--- a/ecosystem-explorer/src/lib/schema-defaults.test.ts
+++ b/ecosystem-explorer/src/lib/schema-defaults.test.ts
@@ -128,64 +128,6 @@ describe("findNodeByPath", () => {
expect(findNodeByPath(rootSchema, ["nonexistent"])).toBeUndefined();
});
- it("returns the circular_ref node and stops descending", () => {
- const samplerSchema: ConfigNode = {
- controlType: "group",
- key: "root",
- label: "Root",
- path: "",
- children: [
- {
- controlType: "group",
- key: "tracer_provider",
- label: "Tracer Provider",
- path: "tracer_provider",
- children: [
- {
- controlType: "plugin_select",
- key: "sampler",
- label: "Sampler",
- path: "tracer_provider.sampler",
- allowCustom: true,
- options: [
- {
- controlType: "group",
- key: "parent_based",
- label: "Parent Based",
- path: "tracer_provider.sampler.parent_based",
- children: [
- {
- controlType: "circular_ref",
- key: "root",
- label: "Root",
- path: "tracer_provider.sampler.parent_based.root",
- refType: "Sampler",
- } as CircularRefNode,
- ],
- },
- ],
- } as PluginSelectNode,
- ],
- },
- ],
- };
- const direct = findNodeByPath(samplerSchema, [
- "tracer_provider",
- "sampler",
- "parent_based",
- "root",
- ]);
- expect(direct?.controlType).toBe("circular_ref");
- const past = findNodeByPath(samplerSchema, [
- "tracer_provider",
- "sampler",
- "parent_based",
- "root",
- "always_on",
- ]);
- expect(past?.controlType).toBe("circular_ref");
- });
-
it("descends into union variants by key", () => {
const schema: ConfigNode = {
controlType: "group",
diff --git a/ecosystem-explorer/src/lib/schema-defaults.ts b/ecosystem-explorer/src/lib/schema-defaults.ts
index 1b5af20c..33e170b8 100644
--- a/ecosystem-explorer/src/lib/schema-defaults.ts
+++ b/ecosystem-explorer/src/lib/schema-defaults.ts
@@ -102,7 +102,6 @@ export function findNodeByPath(
for (const segment of segments) {
if (!current) return undefined;
- if (current.controlType === "circular_ref") return current;
if (typeof segment === "number") {
if (current.controlType === "list") {
current = (current as ListNode).itemSchema;
diff --git a/ecosystem-explorer/src/lib/yaml-generator.test.ts b/ecosystem-explorer/src/lib/yaml-generator.test.ts
index 9dff8a36..b2011558 100644
--- a/ecosystem-explorer/src/lib/yaml-generator.test.ts
+++ b/ecosystem-explorer/src/lib/yaml-generator.test.ts
@@ -35,23 +35,16 @@ const emptyState: ConfigurationBuilderState = {
};
describe("generateYaml", () => {
- it("generates default header with schema version, file-loader hint, and respects override", () => {
+ it("generates default header and respects override", () => {
const defaultOutput = generateYaml(emptyState, emptySchema);
+ expect(defaultOutput).toContain("# OpenTelemetry SDK Configuration");
expect(defaultOutput).toContain("# Schema version: 1.0.0");
- expect(defaultOutput).toContain("# -Dotel.config.file=/path/to/otel-config.yaml");
- expect(defaultOutput).not.toContain("Java agent:");
const overridden = generateYaml(emptyState, emptySchema, { header: "# custom" });
expect(overridden.startsWith("# custom\n")).toBe(true);
expect(overridden).not.toContain("# OpenTelemetry SDK Configuration");
});
- it("includes the Java agent version when supplied via options", () => {
- const output = generateYaml(emptyState, emptySchema, { javaAgentVersion: "2.27.0" });
- expect(output).toContain("# Schema version: 1.0.0");
- expect(output).toContain("Java agent: 2.27.0");
- });
-
const fixtureSchema: ConfigNode = {
controlType: "group",
key: "root",
@@ -116,8 +109,6 @@ describe("generateYaml", () => {
expect(output).toContain('file_format: "1.0"');
expect(output).not.toContain("file_format: 1.0.1");
- expect(output).not.toMatch(/^#[^\n]*\n[^\n]*file_format:/m);
- expect(output).not.toContain("# File Format");
const fileFormatIdx = output.indexOf("file_format:");
const loggerIdx = output.indexOf("logger_provider:");
@@ -129,9 +120,9 @@ describe("generateYaml", () => {
expect(resourceIdx).toBeGreaterThan(loggerIdx);
expect(tracerIdx).toBeGreaterThan(resourceIdx);
- expect(output).toContain("# Logger Provider: Configure logger provider.");
- expect(output).toContain("# Resource: Configure resource for all signals.");
- expect(output).toContain("# Tracer Provider: Configure tracer provider.");
+ expect(output).toContain("# Logger Provider — Configure logger provider.");
+ expect(output).toContain("# Resource — Configure resource for all signals.");
+ expect(output).toContain("# Tracer Provider — Configure tracer provider.");
expect(output).not.toContain("legacy_thing");
});
diff --git a/ecosystem-explorer/src/lib/yaml-generator.ts b/ecosystem-explorer/src/lib/yaml-generator.ts
index d272fb3b..a81e9552 100644
--- a/ecosystem-explorer/src/lib/yaml-generator.ts
+++ b/ecosystem-explorer/src/lib/yaml-generator.ts
@@ -27,26 +27,15 @@ type StrippedResult = ConfigValue | typeof EMPTY;
interface GenerateYamlOptions {
header?: string;
- javaAgentVersion?: string;
}
-function defaultHeader(schemaVersion: string, javaAgentVersion?: string): string {
+function defaultHeader(version: string): string {
const date = new Date().toISOString().slice(0, 10);
- const versionLine = javaAgentVersion
- ? `# Schema version: ${schemaVersion} - Java agent: ${javaAgentVersion}`
- : `# Schema version: ${schemaVersion}`;
return [
"# OpenTelemetry SDK Configuration",
`# Generated by Ecosystem Explorer on ${date}`,
- versionLine,
- "#",
- "# Pass to the agent with:",
- "# -Dotel.config.file=/path/to/otel-config.yaml",
- "#",
- "# NOTE: With the exception of env var substitution syntax (i.e. ${MY_ENV}),",
- "# SDKs ignore environment variables when interpreting config files.",
- "#",
- "# Schema docs: https://github.com/open-telemetry/opentelemetry-configuration/blob/main/schema-docs.md",
+ `# Schema version: ${version}`,
+ "# Docs: https://opentelemetry.io/docs/specs/otel/configuration/",
"",
].join("\n");
}
@@ -90,7 +79,7 @@ function sectionComment(node: ConfigNode | undefined, fallbackKey: string): stri
.filter((l) => l !== "# ");
return `# ${label}\n${lines.join("\n")}\n`;
}
- return `# ${label}: ${desc}\n`;
+ return `# ${label} — ${desc}\n`;
}
function stripEmpties(value: ConfigValue): StrippedResult {
@@ -138,7 +127,7 @@ export function generateYaml(
schema: ConfigNode,
options?: GenerateYamlOptions
): string {
- const header = options?.header ?? defaultHeader(state.version, options?.javaAgentVersion);
+ const header = options?.header ?? defaultHeader(state.version);
if (schema.controlType !== "group") {
const parts = [header, "# Schema is not a group; cannot generate sections.", ""].filter(
@@ -149,7 +138,10 @@ export function generateYaml(
const children = schema.children;
- const fileFormatBlock = dumpYaml({ file_format: toFileFormatVersion(state.version) });
+ const fileFormatNode = children.find((c) => c.key === "file_format");
+ const fileFormatBlock =
+ sectionComment(fileFormatNode, "file_format") +
+ dumpYaml({ file_format: toFileFormatVersion(state.version) });
const others = children
.filter((c) => c.key !== "file_format")
diff --git a/ecosystem-explorer/src/test/integration/configuration-builder-basic.integration.test.tsx b/ecosystem-explorer/src/test/integration/configuration-builder-basic.integration.test.tsx
index add186d2..4a1b45ec 100644
--- a/ecosystem-explorer/src/test/integration/configuration-builder-basic.integration.test.tsx
+++ b/ecosystem-explorer/src/test/integration/configuration-builder-basic.integration.test.tsx
@@ -22,7 +22,7 @@ import { renderBuilderPage as renderPage } from "./helpers/render-builder-page";
beforeAll(() => installFetchInterceptor());
afterAll(() => uninstallFetchInterceptor());
-describe("ConfigurationBuilderPage basic", () => {
+describe("ConfigurationBuilderPage — basic", () => {
it("renders the SDK tab with starter-preloaded sections", async () => {
renderPage();
const resourceToggle = await screen.findByRole(
@@ -112,7 +112,7 @@ describe("ConfigurationBuilderPage basic", () => {
expect(generalCard).not.toBeNull();
const general = within(generalCard!);
expect(general.getByText("General")).toBeInTheDocument();
- // Collapsed by default; leaf fields not in the DOM yet.
+ // Collapsed by default — leaf fields not in the DOM yet.
expect(general.queryByText("Disabled")).toBeNull();
expect(general.queryByText("Log Level")).toBeNull();
// Click the chevron to expand and reveal the leaves.
@@ -135,23 +135,4 @@ describe("ConfigurationBuilderPage basic", () => {
// Its children (e.g. Attribute Value Length Limit) must not be in the DOM.
expect(tracer.queryByText("Attribute Value Length Limit")).toBeNull();
});
-
- it("renders the Beta badge and outbound docs/issue links in the header", async () => {
- renderPage();
- await screen.findByRole("switch", { name: /Enable Resource/i }, { timeout: 10_000 });
- expect(screen.getByText("Beta")).toBeInTheDocument();
-
- const docsLink = screen.getByRole("link", { name: /declarative configuration/i });
- expect(docsLink).toHaveAttribute(
- "href",
- "https://opentelemetry.io/docs/zero-code/java/agent/declarative-configuration/"
- );
- expect(docsLink).toHaveAttribute("target", "_blank");
-
- const issueLink = screen.getByRole("link", { name: /report an issue/i });
- expect(issueLink).toHaveAttribute(
- "href",
- "https://github.com/open-telemetry/opentelemetry-ecosystem-explorer/issues/new"
- );
- });
});
diff --git a/ecosystem-explorer/src/test/integration/configuration-builder-card-clicks.integration.test.tsx b/ecosystem-explorer/src/test/integration/configuration-builder-card-clicks.integration.test.tsx
index f8bc7be9..4365272d 100644
--- a/ecosystem-explorer/src/test/integration/configuration-builder-card-clicks.integration.test.tsx
+++ b/ecosystem-explorer/src/test/integration/configuration-builder-card-clicks.integration.test.tsx
@@ -30,20 +30,21 @@ beforeEach(() => {
cleanup();
});
-describe("ConfigurationBuilderPage card click behavior", () => {
+describe("ConfigurationBuilderPage — card click behavior", () => {
it("clicking an input inside an expanded card does not steal focus or scroll", async () => {
renderPage();
const user = userEvent.setup();
// Wait for the page to settle. Resource is auto-enabled by the starter
- // and its attributes_list text input renders inline.
+ // and its service.name Value union renders a text input inline.
await screen.findByRole("switch", { name: /Enable Resource/i }, { timeout: 10_000 });
const resourceSection = document.querySelector('[data-section-key="resource"]');
expect(resourceSection).not.toBeNull();
const resource = within(resourceSection as HTMLElement);
- // The attributes_list text input is in the DOM as soon as Resource expands.
+ // The Value union for service.name is defaultExpanded with the Text
+ // variant selected; its text input is in the DOM straight away.
await waitFor(() => {
expect(resource.queryAllByRole("textbox").length).toBeGreaterThan(0);
});
diff --git a/ecosystem-explorer/src/test/integration/configuration-builder-instrumentation-version.integration.test.tsx b/ecosystem-explorer/src/test/integration/configuration-builder-instrumentation-version.integration.test.tsx
deleted file mode 100644
index 61cabb98..00000000
--- a/ecosystem-explorer/src/test/integration/configuration-builder-instrumentation-version.integration.test.tsx
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- *
- * Licensed 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.
- */
-import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
-import { screen, waitFor, within } from "@testing-library/react";
-import userEvent from "@testing-library/user-event";
-import schemaVersionsIndex from "../../../public/data/configuration/versions-index.json";
-import javaAgentVersionsIndex from "../../../public/data/javaagent/versions-index.json";
-import { installFetchInterceptor, uninstallFetchInterceptor } from "./helpers/fetch-interceptor";
-import { renderBuilderPage as renderPage } from "./helpers/render-builder-page";
-
-const latestSchemaVersion = schemaVersionsIndex.versions.find((v) => v.is_latest)!.version;
-const latestAgentVersion = javaAgentVersionsIndex.versions.find((v) => v.is_latest)!.version;
-const otherAgentVersion = javaAgentVersionsIndex.versions.find((v) => !v.is_latest)?.version;
-
-beforeAll(() => installFetchInterceptor());
-afterAll(() => uninstallFetchInterceptor());
-beforeEach(() => localStorage.clear());
-
-async function findAgentSelector(): Promise {
- return (await screen.findByLabelText("Agent", {}, { timeout: 10_000 })) as HTMLSelectElement;
-}
-
-async function openInstrumentationTab(user: ReturnType) {
- await screen.findByRole("switch", { name: /Enable Resource/i }, { timeout: 10_000 });
- const sidebar = screen.getByRole("complementary");
- await user.click(within(sidebar).getByRole("tab", { name: /Instrumentation/i }));
-}
-
-describe("ConfigurationBuilderPage version selectors", () => {
- it("renders Schema and Agent selectors side by side in the page header", async () => {
- renderPage();
- const schema = (await screen.findByLabelText(
- "Schema",
- {},
- { timeout: 10_000 }
- )) as HTMLSelectElement;
- const agent = await findAgentSelector();
- expect(schema.value).toBe(latestSchemaVersion);
- expect(agent.value).toBe(latestAgentVersion);
- });
-
- it("re-runs the registry lookup and updates the Instrumentation tab when the Agent version changes", async () => {
- if (!otherAgentVersion) return;
- renderPage();
- const user = userEvent.setup();
- const agent = await findAgentSelector();
-
- await openInstrumentationTab(user);
- const reactorRow = (await screen.findByTestId(
- "instrumentation-row-reactor",
- {},
- { timeout: 10_000 }
- )) as HTMLElement;
- expect(within(reactorRow).getByText("2 versions")).toBeInTheDocument();
-
- await user.selectOptions(agent, otherAgentVersion);
-
- await waitFor(
- () => {
- const updated = screen.getByTestId("instrumentation-row-reactor");
- expect(within(updated).queryByText("2 versions")).toBeNull();
- },
- { timeout: 10_000 }
- );
- });
-
- it("updates the YAML preview header from the SDK tab when the Agent version changes", async () => {
- if (!otherAgentVersion) return;
- renderPage();
- const user = userEvent.setup();
- const preview = (await screen.findByLabelText(
- "Output Preview",
- {},
- { timeout: 10_000 }
- )) as HTMLElement;
- expect(preview.textContent).toContain(`Java agent: ${latestAgentVersion}`);
-
- const agent = await findAgentSelector();
- await user.selectOptions(agent, otherAgentVersion);
-
- await waitFor(() => {
- expect(preview.textContent).toContain(`Java agent: ${otherAgentVersion}`);
- });
- expect(preview.textContent).not.toContain(`Java agent: ${latestAgentVersion}`);
- });
-
- it("preserves user-entered configuration values when the Agent version changes", async () => {
- if (!otherAgentVersion) return;
- renderPage();
- const user = userEvent.setup();
- const resourceToggle = await screen.findByRole(
- "switch",
- { name: /Enable Resource/i },
- { timeout: 10_000 }
- );
- expect(resourceToggle).toHaveAttribute("aria-checked", "true");
- await user.click(resourceToggle);
- await waitFor(() => expect(resourceToggle).toHaveAttribute("aria-checked", "false"));
-
- const agent = await findAgentSelector();
- await user.selectOptions(agent, otherAgentVersion);
-
- expect(resourceToggle).toHaveAttribute("aria-checked", "false");
- });
-
- it("does not persist the Agent selection: remount resets to latest with empty localStorage", async () => {
- if (!otherAgentVersion) return;
- const { unmount } = renderPage();
- const user = userEvent.setup();
- const agent = await findAgentSelector();
- await user.selectOptions(agent, otherAgentVersion);
- expect(agent.value).toBe(otherAgentVersion);
-
- unmount();
- expect(localStorage.length).toBe(0);
-
- renderPage();
- const reloaded = await findAgentSelector();
- expect(reloaded.value).toBe(latestAgentVersion);
- });
-});
diff --git a/ecosystem-explorer/src/types/javaagent.ts b/ecosystem-explorer/src/types/javaagent.ts
index 551dcde2..e1d75cae 100644
--- a/ecosystem-explorer/src/types/javaagent.ts
+++ b/ecosystem-explorer/src/types/javaagent.ts
@@ -62,8 +62,6 @@ export interface InstrumentationData {
configurations?: Configuration[];
/** Telemetry emitted by this instrumentation under specific conditions. */
telemetry?: Telemetry[];
- /** Content hash of the library README markdown file. */
- markdown_hash?: string;
/** Whether this is a custom (non-upstream) instrumentation. */
_is_custom?: boolean;
}
diff --git a/ecosystem-explorer/src/v1/V1App.tsx b/ecosystem-explorer/src/v1/V1App.tsx
index a9d7313f..3ca06d7a 100644
--- a/ecosystem-explorer/src/v1/V1App.tsx
+++ b/ecosystem-explorer/src/v1/V1App.tsx
@@ -15,16 +15,16 @@
*/
import { lazy, Suspense } from "react";
import { Routes, Route } from "react-router-dom";
+import { Footer } from "@/components/layout/footer";
import { ErrorBoundary } from "@/components/ui/error-boundary";
import { isEnabled } from "@/lib/feature-flags";
-import { CncfCallout } from "@/v1/components/layout/cncf-callout";
-import { FooterV1 } from "@/v1/components/layout/footer";
import { NavBar } from "@/v1/components/layout/nav-bar";
import "@/v1/styles/index.css";
/*
* V1 sub-app entry. Reached via the V1_REDESIGN boundary read in `src/App.tsx`.
- * Owns its own `` and v1 chrome (navbar, CNCF callout, footer).
+ * Owns its own `` and v1 chrome. The legacy `` is reused as a
+ * temporary placeholder until PR 6 ships `` + ``.
*
* The `.v1-app` wrapper class scopes v1-specific surface-token overrides defined
* in `src/v1/styles/tokens.css` so they don't leak into the legacy app.
@@ -117,10 +117,12 @@ export function V1App() {
{isEnabled("JAVA_RELEASE_COMPARISON") && (
} />
)}
- }
- />
+ {isEnabled("JAVA_CONFIG_BUILDER") && (
+ }
+ />
+ )}
} />
{isEnabled("COLLECTOR_PAGE") && (
<>
@@ -137,8 +139,7 @@ export function V1App() {
-
-
+
);
}
diff --git a/ecosystem-explorer/src/v1/components/icons/bluesky-icon.tsx b/ecosystem-explorer/src/v1/components/icons/bluesky-icon.tsx
deleted file mode 100644
index 1abf7dc1..00000000
--- a/ecosystem-explorer/src/v1/components/icons/bluesky-icon.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- *
- * Licensed 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.
- */
-
-/*
- * Font Awesome 6 free-brands `bluesky` mark, inlined so we don't pull the full
- * Font Awesome bundle for a handful of footer icons (see decision log
- * 2026-05-06, foundation audit Q3). Icon licensed CC BY 4.0 by Fonticons, Inc.
- */
-export function BlueskyIcon({ className }: { className?: string }) {
- return (
-
- );
-}
diff --git a/ecosystem-explorer/src/v1/components/icons/cncf-logo.tsx b/ecosystem-explorer/src/v1/components/icons/cncf-logo.tsx
deleted file mode 100644
index 891c3225..00000000
--- a/ecosystem-explorer/src/v1/components/icons/cncf-logo.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- *
- * Licensed 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.
- */
-
-/*
- * CNCF "Cloud Native Computing Foundation" wordmark + mark, verbatim from
- * opentelemetry.io's `static/img/logos/cncf-white.svg` (cncf/artwork upstream).
- * Fill is white-ish (`#fff` baseline via the original `.st0` class), and we
- * pin it via `fill="currentColor"` so consumers can recolor it with text-*
- * classes — on the dark CncfCallout band it renders white.
- */
-export function CncfLogo({ className }: { className?: string }) {
- return (
-
- );
-}
diff --git a/ecosystem-explorer/src/v1/components/icons/github-icon.tsx b/ecosystem-explorer/src/v1/components/icons/github-icon.tsx
deleted file mode 100644
index 0e0b4f44..00000000
--- a/ecosystem-explorer/src/v1/components/icons/github-icon.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- *
- * Licensed 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.
- */
-
-/*
- * Font Awesome 6 free-brands `github` mark. Inlined because Lucide v1.x dropped
- * brand glyphs for licensing reasons. Icon licensed CC BY 4.0 by Fonticons,
- * Inc.
- */
-export function GitHubIcon({ className }: { className?: string }) {
- return (
-
- );
-}
diff --git a/ecosystem-explorer/src/v1/components/icons/mastodon-icon.tsx b/ecosystem-explorer/src/v1/components/icons/mastodon-icon.tsx
deleted file mode 100644
index 391316c3..00000000
--- a/ecosystem-explorer/src/v1/components/icons/mastodon-icon.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- *
- * Licensed 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.
- */
-
-/*
- * Font Awesome 6 free-brands `mastodon` mark, inlined to avoid the full Font
- * Awesome bundle (decision log 2026-05-06). Icon licensed CC BY 4.0 by
- * Fonticons, Inc.
- */
-export function MastodonIcon({ className }: { className?: string }) {
- return (
-
- );
-}
diff --git a/ecosystem-explorer/src/v1/components/icons/slack-icon.tsx b/ecosystem-explorer/src/v1/components/icons/slack-icon.tsx
deleted file mode 100644
index a3e69297..00000000
--- a/ecosystem-explorer/src/v1/components/icons/slack-icon.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- *
- * Licensed 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.
- */
-
-/*
- * Font Awesome 6 free-brands `slack` mark. Inlined because Lucide v1.x dropped
- * brand glyphs for licensing reasons. Icon licensed CC BY 4.0 by Fonticons,
- * Inc.
- */
-export function SlackIcon({ className }: { className?: string }) {
- return (
-
- );
-}
diff --git a/ecosystem-explorer/src/v1/components/icons/stack-overflow-icon.tsx b/ecosystem-explorer/src/v1/components/icons/stack-overflow-icon.tsx
deleted file mode 100644
index 95fd3a9f..00000000
--- a/ecosystem-explorer/src/v1/components/icons/stack-overflow-icon.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- *
- * Licensed 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.
- */
-
-/*
- * Font Awesome 6 free-brands `stack-overflow` mark, inlined to avoid the full
- * Font Awesome bundle (decision log 2026-05-06). Icon licensed CC BY 4.0 by
- * Fonticons, Inc.
- */
-export function StackOverflowIcon({ className }: { className?: string }) {
- return (
-
- );
-}
diff --git a/ecosystem-explorer/src/v1/components/icons/trademark-icon.tsx b/ecosystem-explorer/src/v1/components/icons/trademark-icon.tsx
deleted file mode 100644
index ee4dda61..00000000
--- a/ecosystem-explorer/src/v1/components/icons/trademark-icon.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- *
- * Licensed 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.
- */
-
-/*
- * Font Awesome 6 free-solid `trademark` mark, inlined because Lucide does not
- * ship a trademark glyph (see footer icon strategy, decision log 2026-05-06).
- * Icon licensed CC BY 4.0 by Fonticons, Inc.
- */
-export function TrademarkIcon({ className }: { className?: string }) {
- return (
-
- );
-}
diff --git a/ecosystem-explorer/src/v1/components/layout/cncf-callout.test.tsx b/ecosystem-explorer/src/v1/components/layout/cncf-callout.test.tsx
deleted file mode 100644
index ceca1425..00000000
--- a/ecosystem-explorer/src/v1/components/layout/cncf-callout.test.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- *
- * Licensed 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.
- */
-import { render, screen } from "@testing-library/react";
-import { describe, it, expect } from "vitest";
-import { CncfCallout } from "./cncf-callout";
-
-describe("CncfCallout", () => {
- it("renders the CNCF graduated-project statement", () => {
- render();
-
- expect(screen.getByText(/OpenTelemetry is a/i)).toBeInTheDocument();
- expect(screen.getByText(/graduated project/i)).toBeInTheDocument();
- expect(
- screen.getByText(/merger of the OpenTracing and OpenCensus projects/i)
- ).toBeInTheDocument();
- });
-
- it('links "CNCF" to cncf.io and opens it in a new tab', () => {
- render();
-
- const cncfLink = screen.getByRole("link", { name: "CNCF" });
- expect(cncfLink).toHaveAttribute("href", "https://cncf.io");
- expect(cncfLink).toHaveAttribute("target", "_blank");
- expect(cncfLink.getAttribute("rel")).toMatch(/\bnoopener\b/);
- });
-
- it("renders the CNCF wordmark with an accessible name", () => {
- render();
-
- expect(
- screen.getByRole("img", { name: /Cloud Native Computing Foundation/i })
- ).toBeInTheDocument();
- });
-
- it("exposes the callout as a labelled region", () => {
- render();
-
- expect(
- screen.getByRole("region", { name: /OpenTelemetry is a.*graduated project/i })
- ).toBeInTheDocument();
- });
-});
diff --git a/ecosystem-explorer/src/v1/components/layout/cncf-callout.tsx b/ecosystem-explorer/src/v1/components/layout/cncf-callout.tsx
deleted file mode 100644
index 75249a24..00000000
--- a/ecosystem-explorer/src/v1/components/layout/cncf-callout.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- *
- * Licensed 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.
- */
-
-/*
- * CncfCallout — explorer-original chrome that sits above FooterV1 on every
- * v1 route. No upstream opentelemetry.io equivalent (grep'd: zero hits in
- * `layouts/_partials/` or `themes/docsy/`). Content follows the redesign
- * brief (`projects/84-ui-ux-design/ecosystem-explorer-v1-design-brief.md`
- * lines 55, 82) and the mockup at `ecosystem-explorer-v1-mockups.html:1738`.
- *
- * Surface uses OTel-secondary (purple) per the brief — `td-box--secondary` —
- * which the design brief identifies as the canonical sitewide-callout color.
- * The mockup uses a neutral `box-muted`; the brief is the more recent intent.
- */
-
-import { CncfLogo } from "@/v1/components/icons/cncf-logo";
-
-export function CncfCallout() {
- return (
-
-
-
-
- OpenTelemetry is a{" "}
-
- CNCF
- {" "}
- graduated project.
-
-
- Formed through a merger of the OpenTracing and OpenCensus projects.
-
-
-
-
- );
-}
diff --git a/ecosystem-explorer/src/v1/components/layout/footer.test.tsx b/ecosystem-explorer/src/v1/components/layout/footer.test.tsx
deleted file mode 100644
index 12a1ddb1..00000000
--- a/ecosystem-explorer/src/v1/components/layout/footer.test.tsx
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- *
- * Licensed 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.
- */
-import { render, screen, within } from "@testing-library/react";
-import { MemoryRouter } from "react-router-dom";
-import { describe, it, expect } from "vitest";
-import { FooterV1 } from "./footer";
-import { ThemeProvider } from "@/theme-context";
-
-function renderFooter() {
- return render(
-
-
-
-
-
- );
-}
-
-const expectedLeftLinks = [
- { name: "Mailing Lists", url: "https://github.com/open-telemetry/community#mailing-lists" },
- { name: "Bluesky", url: "https://bsky.app/profile/opentelemetry.io" },
- { name: "Mastodon", url: "https://fosstodon.org/@opentelemetry" },
- { name: "Stack Overflow", url: "https://stackoverflow.com/questions/tagged/open-telemetry" },
- {
- name: "OTel logos",
- url: "https://github.com/cncf/artwork/tree/master/projects/opentelemetry",
- },
- {
- name: "Meeting Recordings",
- url: "https://docs.google.com/spreadsheets/d/1SYKfjYhZdm2Wh2Cl6KVQalKg_m4NhTPZqq-8SzEVO6s",
- },
- { name: "Site analytics", url: "https://lookerstudio.google.com/s/tSTKxK1ECeU" },
-];
-
-const expectedRightLinks = [
- { name: "GitHub", url: "https://github.com/open-telemetry" },
- { name: "Slack #opentelemetry", url: "https://cloud-native.slack.com/archives/CJFCJHG4Q" },
- {
- name: "CNCF DevStats",
- url: "https://opentelemetry.devstats.cncf.io/d/8/dashboards?orgId=1&refresh=15m",
- },
- { name: "Privacy Policy", url: "https://www.linuxfoundation.org/legal/privacy-policy" },
- { name: "Trademark Usage", url: "https://www.linuxfoundation.org/legal/trademark-usage" },
- { name: "Marketing Guidelines", url: "/community/marketing-guidelines/" },
- { name: "Site-build info", url: "/site/" },
-];
-
-describe("FooterV1", () => {
- it("renders all 7 user (left-cluster) links with the correct hrefs and aria-labels", () => {
- renderFooter();
-
- for (const link of expectedLeftLinks) {
- const a = screen.getByRole("link", { name: link.name });
- expect(a).toHaveAttribute("href", link.url);
- expect(a).toHaveAttribute("title", link.name);
- }
- });
-
- it("renders all 7 developer (right-cluster) links with the correct hrefs and aria-labels", () => {
- renderFooter();
-
- for (const link of expectedRightLinks) {
- const a = screen.getByRole("link", { name: link.name });
- expect(a).toHaveAttribute("href", link.url);
- expect(a).toHaveAttribute("title", link.name);
- }
- });
-
- it("marks external links with target=_blank and rel=noopener noreferrer", () => {
- renderFooter();
-
- const github = screen.getByRole("link", { name: "GitHub" });
- expect(github).toHaveAttribute("target", "_blank");
- const rel = github.getAttribute("rel") ?? "";
- expect(rel).toMatch(/\bnoopener\b/);
- expect(rel).toMatch(/\bnoreferrer\b/);
- });
-
- it("does not open internal links in a new tab", () => {
- renderFooter();
-
- const internal = screen.getByRole("link", { name: "Marketing Guidelines" });
- expect(internal).not.toHaveAttribute("target");
- });
-
- it('preserves rel="me" on the Mastodon link alongside noopener noreferrer', () => {
- renderFooter();
-
- const mastodon = screen.getByRole("link", { name: "Mastodon" });
- const rel = mastodon.getAttribute("rel") ?? "";
- expect(rel).toMatch(/\bme\b/);
- expect(rel).toMatch(/\bnoopener\b/);
- expect(rel).toMatch(/\bnoreferrer\b/);
- });
-
- it("renders the copyright with the CC BY 4.0 link", () => {
- renderFooter();
-
- const ccLink = screen.getByRole("link", { name: /CC BY 4\.0/i });
- expect(ccLink).toHaveAttribute("href", "https://creativecommons.org/licenses/by/4.0");
- expect(ccLink).toHaveAttribute("target", "_blank");
- });
-
- it("renders the copyright text with the year span and authors", () => {
- renderFooter();
-
- const footer = screen.getByRole("contentinfo");
- expect(within(footer).getByText(/2019.*present/)).toBeInTheDocument();
- expect(within(footer).getByText(/OpenTelemetry Authors/)).toBeInTheDocument();
- });
-});
diff --git a/ecosystem-explorer/src/v1/components/layout/footer.tsx b/ecosystem-explorer/src/v1/components/layout/footer.tsx
deleted file mode 100644
index 4e430c97..00000000
--- a/ecosystem-explorer/src/v1/components/layout/footer.tsx
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- *
- * Licensed 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.
- */
-
-/*
- * FooterV1 — mirrors opentelemetry.io's `.td-footer` chrome verbatim.
- *
- * Source rules (in the local opentelemetry.io clone):
- * themes/docsy/layouts/_partials/footer.html — three-column layout
- * themes/docsy/layouts/_partials/footer/{left,right,center,links,copyright}.html
- * themes/docsy/assets/scss/td/_footer.scss — dark surface + spacing
- * config/_default/hugo.yaml — link inventory + copyright
- *
- * Link inventory is locked against the upstream YAML (7 user + 7 developer).
- * Icons follow the locked decision (foundation-audit Q3, 2026-05-06): inline
- * SVG for brand marks Lucide doesn't ship; Lucide for everything else.
- */
-
-import { AreaChart, Book, Hammer, Image, LineChart, Mail, Megaphone, Video } from "lucide-react";
-import { BlueskyIcon } from "@/v1/components/icons/bluesky-icon";
-import { GitHubIcon } from "@/v1/components/icons/github-icon";
-import { MastodonIcon } from "@/v1/components/icons/mastodon-icon";
-import { SlackIcon } from "@/v1/components/icons/slack-icon";
-import { StackOverflowIcon } from "@/v1/components/icons/stack-overflow-icon";
-import { TrademarkIcon } from "@/v1/components/icons/trademark-icon";
-
-type FooterLink = {
- name: string;
- url: string;
- icon: React.ReactNode;
- rel?: string;
-};
-
-const userLinks: FooterLink[] = [
- {
- name: "Mailing Lists",
- url: "https://github.com/open-telemetry/community#mailing-lists",
- icon: ,
- },
- {
- name: "Bluesky",
- url: "https://bsky.app/profile/opentelemetry.io",
- icon: ,
- },
- {
- name: "Mastodon",
- url: "https://fosstodon.org/@opentelemetry",
- icon: ,
- rel: "me",
- },
- {
- name: "Stack Overflow",
- url: "https://stackoverflow.com/questions/tagged/open-telemetry",
- icon: ,
- },
- {
- name: "OTel logos",
- url: "https://github.com/cncf/artwork/tree/master/projects/opentelemetry",
- icon: ,
- },
- {
- name: "Meeting Recordings",
- url: "https://docs.google.com/spreadsheets/d/1SYKfjYhZdm2Wh2Cl6KVQalKg_m4NhTPZqq-8SzEVO6s",
- icon: ,
- },
- {
- name: "Site analytics",
- url: "https://lookerstudio.google.com/s/tSTKxK1ECeU",
- icon: ,
- },
-];
-
-const developerLinks: FooterLink[] = [
- {
- name: "GitHub",
- url: "https://github.com/open-telemetry",
- icon: ,
- },
- {
- name: "Slack #opentelemetry",
- url: "https://cloud-native.slack.com/archives/CJFCJHG4Q",
- icon: ,
- },
- {
- name: "CNCF DevStats",
- url: "https://opentelemetry.devstats.cncf.io/d/8/dashboards?orgId=1&refresh=15m",
- icon: ,
- },
- {
- name: "Privacy Policy",
- url: "https://www.linuxfoundation.org/legal/privacy-policy",
- icon: ,
- },
- {
- name: "Trademark Usage",
- url: "https://www.linuxfoundation.org/legal/trademark-usage",
- icon: ,
- },
- {
- name: "Marketing Guidelines",
- url: "/community/marketing-guidelines/",
- icon: ,
- },
- {
- name: "Site-build info",
- url: "/site/",
- icon: ,
- },
-];
-
-const EXTERNAL_URL = /^https?:\/\//;
-
-function FooterLinks({ items }: { items: FooterLink[] }) {
- return (
-
- );
-}
-
-export function FooterV1() {
- return (
-
- );
-}
diff --git a/ecosystem-explorer/src/v1/components/layout/sub-nav.test.tsx b/ecosystem-explorer/src/v1/components/layout/sub-nav.test.tsx
deleted file mode 100644
index f3828ac6..00000000
--- a/ecosystem-explorer/src/v1/components/layout/sub-nav.test.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- *
- * Licensed 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.
- */
-import { render, screen } from "@testing-library/react";
-import { MemoryRouter } from "react-router-dom";
-import { describe, it, expect } from "vitest";
-import { SubNav } from "./sub-nav";
-
-function renderSubNav(props: Parameters[0]) {
- return render(
-
-
-
- );
-}
-
-describe("SubNav", () => {
- it("renders nothing when both crumbs and actions are empty", () => {
- const { container } = renderSubNav({ crumbs: [] });
- expect(container.firstChild).toBeNull();
- });
-
- it("renders breadcrumb landmark with each crumb in order", () => {
- renderSubNav({
- crumbs: [
- { label: "Collector", href: "/collector" },
- { label: "Components", href: "/collector/components" },
- { label: "otlp" },
- ],
- });
-
- const nav = screen.getByRole("navigation", { name: /breadcrumb/i });
- expect(nav).toBeInTheDocument();
-
- expect(screen.getByRole("link", { name: "Collector" })).toHaveAttribute("href", "/collector");
- expect(screen.getByRole("link", { name: "Components" })).toHaveAttribute(
- "href",
- "/collector/components"
- );
- expect(screen.getByText("otlp")).toHaveAttribute("aria-current", "page");
- });
-
- it("renders the last crumb as a non-link with aria-current=page", () => {
- renderSubNav({
- crumbs: [{ label: "Home", href: "/" }, { label: "Detail" }],
- });
-
- expect(screen.queryByRole("link", { name: "Detail" })).toBeNull();
- expect(screen.getByText("Detail")).toHaveAttribute("aria-current", "page");
- });
-
- it("renders actions in the right-aligned slot", () => {
- renderSubNav({
- crumbs: [{ label: "Home", href: "/" }],
- actions: ,
- });
-
- expect(screen.getByRole("button", { name: "Filter" })).toBeInTheDocument();
- });
-
- it("renders actions even when there are no crumbs", () => {
- renderSubNav({
- crumbs: [],
- actions: ,
- });
-
- expect(screen.getByRole("button", { name: "Edit" })).toBeInTheDocument();
- });
-});
diff --git a/ecosystem-explorer/src/v1/components/layout/sub-nav.tsx b/ecosystem-explorer/src/v1/components/layout/sub-nav.tsx
deleted file mode 100644
index 80554543..00000000
--- a/ecosystem-explorer/src/v1/components/layout/sub-nav.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- *
- * Licensed 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.
- */
-
-/*
- * SubNav — breadcrumb row that sits below the navbar on inner pages.
- *
- * Mirrors opentelemetry.io's `.breadcrumb` pattern (Bootstrap), but tightened
- * to the explorer's chrome. The right-side `actions` slot is for page-level
- * controls (filter toggles, "Edit on GitHub", etc.). Renders nothing if both
- * `crumbs` and `actions` are empty so callers can mount it unconditionally.
- */
-import { ChevronRight } from "lucide-react";
-import { Link } from "react-router-dom";
-
-export interface BreadcrumbItem {
- label: string;
- href?: string;
-}
-
-export interface SubNavProps {
- crumbs: BreadcrumbItem[];
- actions?: React.ReactNode;
- className?: string;
-}
-
-export function SubNav({ crumbs, actions, className }: SubNavProps) {
- if (crumbs.length === 0 && !actions) return null;
-
- return (
-
-
- {crumbs.length > 0 && (
-
- )}
- {actions &&
{actions}
}
-
-
- );
-}
diff --git a/ecosystem-explorer/src/v1/styles/cncf-callout.css b/ecosystem-explorer/src/v1/styles/cncf-callout.css
deleted file mode 100644
index 14fbac93..00000000
--- a/ecosystem-explorer/src/v1/styles/cncf-callout.css
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- *
- * Licensed 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.
- */
-
-/*
- * CncfCallout — explorer-original chrome between `` and ``.
- * No upstream equivalent on opentelemetry.io.
- *
- * Surface: OTel-secondary (purple `#4f62ad`) per the design brief
- * (`projects/84-ui-ux-design/ecosystem-explorer-v1-design-brief.md` line 55),
- * not the mockup's neutral `box-muted`.
- */
-
-.td-cncf-callout {
- font-size: 16px;
- background-color: #4f62ad; /* OTel purple — secondary */
- color: #fff;
- padding: 2rem 0;
- text-align: center;
-}
-
-.td-cncf-callout__container {
- width: 100%;
- max-width: 1320px;
- padding-right: 0.75rem;
- padding-left: 0.75rem;
- margin-right: auto;
- margin-left: auto;
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 1rem;
-}
-
-.td-cncf-callout__lead {
- font-size: 0.875rem;
- line-height: 1.5;
- margin: 0;
-}
-
-.td-cncf-callout__lead strong {
- font-weight: 600;
-}
-
-.td-cncf-callout__link {
- color: inherit;
- text-decoration: underline;
- text-decoration-thickness: 1px;
- text-underline-offset: 0.2em;
-}
-
-.td-cncf-callout__link:hover,
-.td-cncf-callout__link:focus-visible {
- color: #f5a800; /* OTel orange */
-}
-
-.td-cncf-callout__logo {
- height: 32px;
- width: auto;
- color: #fff;
- opacity: 0.95;
-}
diff --git a/ecosystem-explorer/src/v1/styles/footer.css b/ecosystem-explorer/src/v1/styles/footer.css
deleted file mode 100644
index c070fa14..00000000
--- a/ecosystem-explorer/src/v1/styles/footer.css
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- *
- * Licensed 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.
- */
-
-/*
- * Footer — mirrors opentelemetry.io's `.td-footer` chrome.
- *
- * Source rules (in the local opentelemetry.io clone):
- * themes/docsy/assets/scss/td/_footer.scss — base sizing, padding, border
- * themes/docsy/assets/scss/td/_boxes.scss — `.td-box--dark` surface tokens
- * themes/docsy/layouts/_partials/footer.html — 3-column responsive layout
- *
- * Pixel-anchored at 16px-rem (the explorer sets `html { font-size: 14px }`
- * globally, so rem-based descendants would shrink). We pin the footer subtree
- * at 16px so `em` resolves correctly and `rem` values match Bootstrap's scale.
- *
- * Surface stays dark in both themes — upstream `.td-footer @extend .td-box--dark`
- * imposes the dark background regardless of `data-bs-theme`. Upstream's top
- * border color flips per theme; the explorer collapses this to a single dark
- * border since the rendered surface doesn't change.
- */
-
-.td-footer {
- font-size: 16px;
- background-color: #212529; /* $dark / $gray-900 — `.td-box--dark` */
- color: #fff; /* color-contrast($gray-900) */
- min-height: 10rem;
- padding-top: 3rem; /* map-get($spacers, 5) */
- padding-bottom: 1rem;
- border-top: 1px solid #495057; /* --bs-border-color / --bs-gray-700 */
-}
-
-.td-footer__container {
- width: 100%;
- max-width: 1320px;
- padding-right: 0.75rem;
- padding-left: 0.75rem;
- margin-right: auto;
- margin-left: auto;
-}
-
-.td-footer__row {
- display: grid;
- gap: 0.75rem 1rem;
- grid-template-columns: 1fr 1fr;
- grid-template-areas:
- "left right"
- "center center";
- align-items: center;
-}
-
-@media (min-width: 576px) {
- .td-footer__row {
- grid-template-columns: 1fr 1fr 1fr;
- grid-template-areas: "left center right";
- }
-}
-
-.td-footer__left {
- grid-area: left;
-}
-
-.td-footer__right {
- grid-area: right;
- text-align: right;
-}
-
-.td-footer__center {
- grid-area: center;
- font-size: 0.875em; /* @extends .small */
- text-align: center;
- padding-top: 0.5rem;
- padding-bottom: 0.5rem;
-}
-
-.td-footer__copyright {
- display: inline;
-}
-
-.td-footer__authors {
- padding-left: 0.25rem;
-}
-
-.td-footer__center a {
- color: inherit;
- text-decoration: underline;
- text-decoration-thickness: 1px;
- text-underline-offset: 0.2em;
-}
-
-.td-footer__center a:hover,
-.td-footer__center a:focus-visible {
- color: #f5a800; /* OTel orange — brand secondary */
-}
-
-.td-footer__links-list {
- list-style: none;
- padding: 0;
- margin: 0;
-}
-
-.td-footer__links-item {
- display: inline-block;
- margin-right: 0.5rem;
- font-size: 1.25rem; /* map-get($font-sizes, 4) */
- line-height: 1;
-}
-
-.td-footer__links-item:last-child {
- margin-right: 0;
-}
-
-.td-footer__links-item a {
- color: inherit;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- text-decoration: none;
- transition: color 0.15s ease-in-out;
-}
-
-.td-footer__links-item a:hover,
-.td-footer__links-item a:focus-visible {
- color: #f5a800; /* OTel orange — brand secondary */
-}
-
-.td-footer__links-item a:focus-visible {
- outline: 2px solid #f5a800;
- outline-offset: 2px;
- border-radius: 2px;
-}
-
-.td-footer__icon {
- width: 1em;
- height: 1em;
- display: inline-block;
- vertical-align: -0.125em;
-}
diff --git a/ecosystem-explorer/src/v1/styles/index.css b/ecosystem-explorer/src/v1/styles/index.css
index 65de58e2..eb65f3cc 100644
--- a/ecosystem-explorer/src/v1/styles/index.css
+++ b/ecosystem-explorer/src/v1/styles/index.css
@@ -20,18 +20,11 @@
* - tokens.css — v1-only surface-token overrides, scoped via `.v1-app`
* - navbar.css — v1 navbar (`.td-navbar`) — mirrors opentelemetry.io
* - theme-toggle.css — v1 theme toggle dropdown — mirrors opentelemetry.io
- * - sub-nav.css — v1 breadcrumb sub-nav row beneath the navbar
- * - cncf-callout.css — sitewide CNCF band above the footer
- * - footer.css — v1 footer (`.td-footer`) — mirrors opentelemetry.io
*
- * All v1 styles use class selectors (`.td-navbar`, `.td-subnav`, `.td-footer`,
- * `.td-cncf-callout`, `.td-light-dark-menu__*`) and only match elements
- * rendered by ``, so they're inert in the legacy bundle even when
- * both branches ship.
+ * Navbar and theme-toggle styles use class selectors (`.td-navbar`,
+ * `.td-light-dark-menu__*`) and only match elements rendered by ``,
+ * so they're inert in the legacy bundle even when both branches ship.
*/
@import "./tokens.css";
@import "./navbar.css";
@import "./theme-toggle.css";
-@import "./sub-nav.css";
-@import "./cncf-callout.css";
-@import "./footer.css";
diff --git a/ecosystem-explorer/src/v1/styles/sub-nav.css b/ecosystem-explorer/src/v1/styles/sub-nav.css
deleted file mode 100644
index 25755a23..00000000
--- a/ecosystem-explorer/src/v1/styles/sub-nav.css
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- *
- * Licensed 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.
- */
-
-/* Pixel-anchored at 16px-rem like the rest of v1 chrome. */
-
-.td-subnav {
- font-size: 16px;
- background-color: hsl(var(--card-hsl));
- border-bottom: 1px solid hsl(var(--border-hsl));
-}
-
-.td-subnav__container {
- width: 100%;
- max-width: 1320px;
- margin: 0 auto;
- padding: 0.5rem 1.5rem;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 1rem;
- min-height: 2.5rem;
- flex-wrap: wrap;
-}
-
-.td-subnav__breadcrumb {
- min-width: 0;
-}
-
-.td-subnav__crumbs {
- list-style: none;
- margin: 0;
- padding: 0;
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: 0.25rem;
- font-size: 0.875rem;
- line-height: 1.25;
-}
-
-.td-subnav__crumb {
- display: inline-flex;
- align-items: center;
- gap: 0.25rem;
-}
-
-.td-subnav__separator {
- width: 0.875rem;
- height: 0.875rem;
- color: hsl(var(--muted-foreground-hsl));
- flex-shrink: 0;
-}
-
-.td-subnav__crumb-label {
- color: hsl(var(--muted-foreground-hsl));
- text-decoration: none;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 24rem;
-}
-
-.td-subnav__crumb-label:hover,
-.td-subnav__crumb-label:focus-visible {
- color: hsl(var(--foreground-hsl));
- text-decoration: underline;
- text-underline-offset: 0.2em;
-}
-
-.td-subnav__crumb-label--current {
- color: hsl(var(--foreground-hsl));
- font-weight: 600;
-}
-
-.td-subnav__actions {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- flex-shrink: 0;
-}
diff --git a/ecosystem-registry/dotnet/v1.15.3+SNAPSHOT/instrumentation.yaml b/ecosystem-registry/dotnet/v1.15.3+SNAPSHOT/instrumentation.yaml
new file mode 100644
index 00000000..1ffcb08c
--- /dev/null
+++ b/ecosystem-registry/dotnet/v1.15.3+SNAPSHOT/instrumentation.yaml
@@ -0,0 +1,228 @@
+modules:
+- name: OpenTelemetry.Api.ProviderBuilderExtensions
+ description: Contains extensions to register OpenTelemetry in applications using
+ Microsoft.Extensions.DependencyInjection
+ type: extension
+ version: 1.15.3
+- name: OpenTelemetry.AutoInstrumentation
+ description: OpenTelemetry Automatic Instrumentation package with all required components
+ to enable automatic instrumentation.
+ type: instrumentation
+ version: 1.16.0-beta.1
+- name: OpenTelemetry.AutoInstrumentation.AspNetCoreBootstrapper
+ description: ASP.NET Core Bootstrapper used by the OpenTelemetry.AutoInstrumentation
+ project.
+ type: instrumentation
+ version: 1.16.0-beta.1
+- name: OpenTelemetry.AutoInstrumentation.BuildTasks
+ description: Build tasks used by the OpenTelemetry.AutoInstrumentation project.
+ type: instrumentation
+ version: 1.16.0-beta.1
+- name: OpenTelemetry.AutoInstrumentation.Loader
+ description: Loader used by the OpenTelemetry.AutoInstrumentation project.
+ type: instrumentation
+ version: 1.16.0-beta.1
+- name: OpenTelemetry.AutoInstrumentation.Runtime.Managed
+ description: Managed components used by the OpenTelemetry.AutoInstrumentation project.
+ type: instrumentation
+ version: 1.16.0-beta.1
+- name: OpenTelemetry.AutoInstrumentation.Runtime.Native
+ description: Native runtime components used by the OpenTelemetry.AutoInstrumentation
+ project.
+ type: instrumentation
+ version: 1.16.0-beta.1
+- name: OpenTelemetry.AutoInstrumentation.StartupHook
+ description: StartupHook used by the OpenTelemetry.AutoInstrumentation project.
+ type: instrumentation
+ version: 1.16.0-beta.1
+- name: OpenTelemetry.Exporter.Console
+ description: Console exporter for OpenTelemetry .NET
+ type: exporter
+ version: 1.15.3
+- name: OpenTelemetry.Exporter.Geneva
+ description: An OpenTelemetry .NET exporter that exports to local ETW or UDS.
+ type: exporter
+ version: 1.15.2
+- name: OpenTelemetry.Exporter.InMemory
+ description: In-memory exporter for OpenTelemetry .NET
+ type: exporter
+ version: 1.15.3
+- name: OpenTelemetry.Exporter.InfluxDB
+ description: An OpenTelemetry .NET exporter that exports to InfluxDB.
+ type: exporter
+ version: 1.0.0-alpha.8
+- name: OpenTelemetry.Exporter.OneCollector
+ description: An OpenTelemetry .NET exporter that sends telemetry to Microsoft OneCollector.
+ type: exporter
+ version: 1.15.1
+- name: OpenTelemetry.Exporter.OpenTelemetryProtocol
+ description: OpenTelemetry protocol exporter for OpenTelemetry .NET
+ type: exporter
+ version: 1.15.3
+- name: OpenTelemetry.Exporter.Prometheus.AspNetCore
+ description: ASP.NET Core middleware for hosting OpenTelemetry .NET Prometheus Exporter
+ type: exporter
+ version: 1.15.3-beta.1
+- name: OpenTelemetry.Exporter.Prometheus.HttpListener
+ description: Stand-alone HttpListener for hosting OpenTelemetry .NET Prometheus
+ Exporter
+ type: exporter
+ version: 1.15.3-beta.1
+- name: OpenTelemetry.Exporter.Zipkin
+ description: Zipkin exporter for OpenTelemetry .NET
+ type: exporter
+ version: 1.15.3
+- name: OpenTelemetry.Extensions
+ description: OpenTelemetry .NET SDK preview features and extensions.
+ type: extension
+ version: 1.15.0-beta.1
+- name: OpenTelemetry.Extensions.AWS
+ description: OpenTelemetry extensions for AWS.
+ type: extension
+ version: 1.15.1
+- name: OpenTelemetry.Extensions.Enrichment
+ description: OpenTelemetry .NET SDK telemetry enrichment.
+ type: extension
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Extensions.Enrichment.AspNetCore
+ description: OpenTelemetry .NET SDK ASP.NET Core telemetry enrichment.
+ type: extension
+ version: 1.15.1-beta.2
+- name: OpenTelemetry.Extensions.Enrichment.Http
+ description: OpenTelemetry .NET SDK HTTP telemetry enrichment.
+ type: extension
+ version: 1.15.1-beta.2
+- name: OpenTelemetry.Extensions.Hosting
+ description: Contains extensions to start OpenTelemetry in applications using Microsoft.Extensions.Hosting
+ type: extension
+ version: 1.15.3
+- name: OpenTelemetry.Extensions.Propagators
+ description: OpenTelemetry Extensions Propagators
+ type: extension
+ version: 1.15.3
+- name: OpenTelemetry.Instrumentation.AWS
+ description: AWS client instrumentation for OpenTelemetry .NET.
+ type: instrumentation
+ version: 1.15.1
+- name: OpenTelemetry.Instrumentation.AWSLambda
+ description: AWS Lambda tracing wrapper for OpenTelemetry .NET.
+ type: instrumentation
+ version: 1.15.1
+- name: OpenTelemetry.Instrumentation.AspNet
+ description: ASP.NET instrumentation for OpenTelemetry .NET.
+ type: instrumentation
+ version: 1.15.2
+- name: OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule
+ description: A module that instruments incoming request with System.Diagnostics.Activity
+ and notifies listeners with DiagnosticsSource.
+ type: instrumentation
+ version: 1.15.2
+- name: OpenTelemetry.Instrumentation.AspNetCore
+ description: ASP.NET Core instrumentation for OpenTelemetry .NET
+ type: instrumentation
+ version: 1.15.2
+- name: OpenTelemetry.Instrumentation.Cassandra
+ description: OpenTelemetry Cassandra Instrumentation.
+ type: instrumentation
+ version: 1.0.0-beta.6
+- name: OpenTelemetry.Instrumentation.ConfluentKafka
+ description: Confluent.Kafka instrumentation for OpenTelemetry .NET.
+ type: instrumentation
+ version: 0.1.0-alpha.6
+- name: OpenTelemetry.Instrumentation.ElasticsearchClient
+ description: Elasticsearch instrumentation for OpenTelemetry .NET.
+ type: instrumentation
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Instrumentation.EntityFrameworkCore
+ description: Microsoft.EntityFrameworkCore instrumentation for OpenTelemetry .NET.
+ type: instrumentation
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Instrumentation.EventCounters
+ description: OpenTelemetry Metrics instrumentation for .NET EventCounters.
+ type: instrumentation
+ version: 1.15.1-alpha.1
+- name: OpenTelemetry.Instrumentation.GrpcCore
+ description: .NET gRPC Core based client and server interceptors for OpenTelemetry.
+ type: instrumentation
+ version: 1.0.0-beta.11
+- name: OpenTelemetry.Instrumentation.GrpcNetClient
+ description: gRPC for .NET client instrumentation for OpenTelemetry .NET.
+ type: instrumentation
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Instrumentation.Hangfire
+ description: OpenTelemetry Hangfire Instrumentation.
+ type: instrumentation
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Instrumentation.Http
+ description: HTTP instrumentation for OpenTelemetry .NET.
+ type: instrumentation
+ version: 1.15.1
+- name: OpenTelemetry.Instrumentation.Owin
+ description: OpenTelemetry instrumentation for OWIN.
+ type: instrumentation
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Instrumentation.Process
+ description: dotnet process instrumentation for OpenTelemetry .NET.
+ type: instrumentation
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Instrumentation.Quartz
+ description: OpenTelemetry Quartz.NET Instrumentation.
+ type: instrumentation
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Instrumentation.Runtime
+ description: .NET runtime instrumentation for OpenTelemetry .NET.
+ type: instrumentation
+ version: 1.15.1
+- name: OpenTelemetry.Instrumentation.ServiceFabricRemoting
+ description: ServiceFabric Remoting instrumentation for OpenTelemetry .NET.
+ type: instrumentation
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Instrumentation.SqlClient
+ description: SqlClient instrumentation for OpenTelemetry .NET.
+ type: instrumentation
+ version: 1.15.2
+- name: OpenTelemetry.Instrumentation.StackExchangeRedis
+ description: StackExchange.Redis instrumentation for OpenTelemetry .NET.
+ type: instrumentation
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Instrumentation.Wcf
+ description: OpenTelemetry instrumentation for WCF.
+ type: instrumentation
+ version: 1.15.1-beta.2
+- name: OpenTelemetry.Resources.AWS
+ description: OpenTelemetry Resource Detectors for AWS ElasticBeanstalk, EC2, ECS,
+ EKS.
+ type: extension
+ version: 1.15.1
+- name: OpenTelemetry.Resources.Azure
+ description: OpenTelemetry Resource Detectors for Azure cloud environments.
+ type: extension
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Resources.Container
+ description: OpenTelemetry Resource Detectors for Container environment.
+ type: extension
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Resources.Gcp
+ description: OpenTelemetry Resource Detectors for Google Cloud Platform environments.
+ type: extension
+ version: 1.0.0-alpha.1
+- name: OpenTelemetry.Resources.Host
+ description: OpenTelemetry Resource Detectors for Host.
+ type: extension
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Resources.OperatingSystem
+ description: OpenTelemetry Resource Detectors for Operating System.
+ type: extension
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Resources.Process
+ description: OpenTelemetry Resource Detectors for Process.
+ type: extension
+ version: 1.15.1-beta.2
+- name: OpenTelemetry.Resources.ProcessRuntime
+ description: OpenTelemetry Resource Detectors for Process Runtime.
+ type: extension
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Sampler.AWS
+ description: OpenTelemetry remote sampler for AWS X-Ray.
+ type: extension
+ version: 0.1.0-alpha.9
diff --git a/ecosystem-registry/dotnet/v1.15.3/instrumentation.yaml b/ecosystem-registry/dotnet/v1.15.3/instrumentation.yaml
new file mode 100644
index 00000000..1ffcb08c
--- /dev/null
+++ b/ecosystem-registry/dotnet/v1.15.3/instrumentation.yaml
@@ -0,0 +1,228 @@
+modules:
+- name: OpenTelemetry.Api.ProviderBuilderExtensions
+ description: Contains extensions to register OpenTelemetry in applications using
+ Microsoft.Extensions.DependencyInjection
+ type: extension
+ version: 1.15.3
+- name: OpenTelemetry.AutoInstrumentation
+ description: OpenTelemetry Automatic Instrumentation package with all required components
+ to enable automatic instrumentation.
+ type: instrumentation
+ version: 1.16.0-beta.1
+- name: OpenTelemetry.AutoInstrumentation.AspNetCoreBootstrapper
+ description: ASP.NET Core Bootstrapper used by the OpenTelemetry.AutoInstrumentation
+ project.
+ type: instrumentation
+ version: 1.16.0-beta.1
+- name: OpenTelemetry.AutoInstrumentation.BuildTasks
+ description: Build tasks used by the OpenTelemetry.AutoInstrumentation project.
+ type: instrumentation
+ version: 1.16.0-beta.1
+- name: OpenTelemetry.AutoInstrumentation.Loader
+ description: Loader used by the OpenTelemetry.AutoInstrumentation project.
+ type: instrumentation
+ version: 1.16.0-beta.1
+- name: OpenTelemetry.AutoInstrumentation.Runtime.Managed
+ description: Managed components used by the OpenTelemetry.AutoInstrumentation project.
+ type: instrumentation
+ version: 1.16.0-beta.1
+- name: OpenTelemetry.AutoInstrumentation.Runtime.Native
+ description: Native runtime components used by the OpenTelemetry.AutoInstrumentation
+ project.
+ type: instrumentation
+ version: 1.16.0-beta.1
+- name: OpenTelemetry.AutoInstrumentation.StartupHook
+ description: StartupHook used by the OpenTelemetry.AutoInstrumentation project.
+ type: instrumentation
+ version: 1.16.0-beta.1
+- name: OpenTelemetry.Exporter.Console
+ description: Console exporter for OpenTelemetry .NET
+ type: exporter
+ version: 1.15.3
+- name: OpenTelemetry.Exporter.Geneva
+ description: An OpenTelemetry .NET exporter that exports to local ETW or UDS.
+ type: exporter
+ version: 1.15.2
+- name: OpenTelemetry.Exporter.InMemory
+ description: In-memory exporter for OpenTelemetry .NET
+ type: exporter
+ version: 1.15.3
+- name: OpenTelemetry.Exporter.InfluxDB
+ description: An OpenTelemetry .NET exporter that exports to InfluxDB.
+ type: exporter
+ version: 1.0.0-alpha.8
+- name: OpenTelemetry.Exporter.OneCollector
+ description: An OpenTelemetry .NET exporter that sends telemetry to Microsoft OneCollector.
+ type: exporter
+ version: 1.15.1
+- name: OpenTelemetry.Exporter.OpenTelemetryProtocol
+ description: OpenTelemetry protocol exporter for OpenTelemetry .NET
+ type: exporter
+ version: 1.15.3
+- name: OpenTelemetry.Exporter.Prometheus.AspNetCore
+ description: ASP.NET Core middleware for hosting OpenTelemetry .NET Prometheus Exporter
+ type: exporter
+ version: 1.15.3-beta.1
+- name: OpenTelemetry.Exporter.Prometheus.HttpListener
+ description: Stand-alone HttpListener for hosting OpenTelemetry .NET Prometheus
+ Exporter
+ type: exporter
+ version: 1.15.3-beta.1
+- name: OpenTelemetry.Exporter.Zipkin
+ description: Zipkin exporter for OpenTelemetry .NET
+ type: exporter
+ version: 1.15.3
+- name: OpenTelemetry.Extensions
+ description: OpenTelemetry .NET SDK preview features and extensions.
+ type: extension
+ version: 1.15.0-beta.1
+- name: OpenTelemetry.Extensions.AWS
+ description: OpenTelemetry extensions for AWS.
+ type: extension
+ version: 1.15.1
+- name: OpenTelemetry.Extensions.Enrichment
+ description: OpenTelemetry .NET SDK telemetry enrichment.
+ type: extension
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Extensions.Enrichment.AspNetCore
+ description: OpenTelemetry .NET SDK ASP.NET Core telemetry enrichment.
+ type: extension
+ version: 1.15.1-beta.2
+- name: OpenTelemetry.Extensions.Enrichment.Http
+ description: OpenTelemetry .NET SDK HTTP telemetry enrichment.
+ type: extension
+ version: 1.15.1-beta.2
+- name: OpenTelemetry.Extensions.Hosting
+ description: Contains extensions to start OpenTelemetry in applications using Microsoft.Extensions.Hosting
+ type: extension
+ version: 1.15.3
+- name: OpenTelemetry.Extensions.Propagators
+ description: OpenTelemetry Extensions Propagators
+ type: extension
+ version: 1.15.3
+- name: OpenTelemetry.Instrumentation.AWS
+ description: AWS client instrumentation for OpenTelemetry .NET.
+ type: instrumentation
+ version: 1.15.1
+- name: OpenTelemetry.Instrumentation.AWSLambda
+ description: AWS Lambda tracing wrapper for OpenTelemetry .NET.
+ type: instrumentation
+ version: 1.15.1
+- name: OpenTelemetry.Instrumentation.AspNet
+ description: ASP.NET instrumentation for OpenTelemetry .NET.
+ type: instrumentation
+ version: 1.15.2
+- name: OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule
+ description: A module that instruments incoming request with System.Diagnostics.Activity
+ and notifies listeners with DiagnosticsSource.
+ type: instrumentation
+ version: 1.15.2
+- name: OpenTelemetry.Instrumentation.AspNetCore
+ description: ASP.NET Core instrumentation for OpenTelemetry .NET
+ type: instrumentation
+ version: 1.15.2
+- name: OpenTelemetry.Instrumentation.Cassandra
+ description: OpenTelemetry Cassandra Instrumentation.
+ type: instrumentation
+ version: 1.0.0-beta.6
+- name: OpenTelemetry.Instrumentation.ConfluentKafka
+ description: Confluent.Kafka instrumentation for OpenTelemetry .NET.
+ type: instrumentation
+ version: 0.1.0-alpha.6
+- name: OpenTelemetry.Instrumentation.ElasticsearchClient
+ description: Elasticsearch instrumentation for OpenTelemetry .NET.
+ type: instrumentation
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Instrumentation.EntityFrameworkCore
+ description: Microsoft.EntityFrameworkCore instrumentation for OpenTelemetry .NET.
+ type: instrumentation
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Instrumentation.EventCounters
+ description: OpenTelemetry Metrics instrumentation for .NET EventCounters.
+ type: instrumentation
+ version: 1.15.1-alpha.1
+- name: OpenTelemetry.Instrumentation.GrpcCore
+ description: .NET gRPC Core based client and server interceptors for OpenTelemetry.
+ type: instrumentation
+ version: 1.0.0-beta.11
+- name: OpenTelemetry.Instrumentation.GrpcNetClient
+ description: gRPC for .NET client instrumentation for OpenTelemetry .NET.
+ type: instrumentation
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Instrumentation.Hangfire
+ description: OpenTelemetry Hangfire Instrumentation.
+ type: instrumentation
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Instrumentation.Http
+ description: HTTP instrumentation for OpenTelemetry .NET.
+ type: instrumentation
+ version: 1.15.1
+- name: OpenTelemetry.Instrumentation.Owin
+ description: OpenTelemetry instrumentation for OWIN.
+ type: instrumentation
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Instrumentation.Process
+ description: dotnet process instrumentation for OpenTelemetry .NET.
+ type: instrumentation
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Instrumentation.Quartz
+ description: OpenTelemetry Quartz.NET Instrumentation.
+ type: instrumentation
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Instrumentation.Runtime
+ description: .NET runtime instrumentation for OpenTelemetry .NET.
+ type: instrumentation
+ version: 1.15.1
+- name: OpenTelemetry.Instrumentation.ServiceFabricRemoting
+ description: ServiceFabric Remoting instrumentation for OpenTelemetry .NET.
+ type: instrumentation
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Instrumentation.SqlClient
+ description: SqlClient instrumentation for OpenTelemetry .NET.
+ type: instrumentation
+ version: 1.15.2
+- name: OpenTelemetry.Instrumentation.StackExchangeRedis
+ description: StackExchange.Redis instrumentation for OpenTelemetry .NET.
+ type: instrumentation
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Instrumentation.Wcf
+ description: OpenTelemetry instrumentation for WCF.
+ type: instrumentation
+ version: 1.15.1-beta.2
+- name: OpenTelemetry.Resources.AWS
+ description: OpenTelemetry Resource Detectors for AWS ElasticBeanstalk, EC2, ECS,
+ EKS.
+ type: extension
+ version: 1.15.1
+- name: OpenTelemetry.Resources.Azure
+ description: OpenTelemetry Resource Detectors for Azure cloud environments.
+ type: extension
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Resources.Container
+ description: OpenTelemetry Resource Detectors for Container environment.
+ type: extension
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Resources.Gcp
+ description: OpenTelemetry Resource Detectors for Google Cloud Platform environments.
+ type: extension
+ version: 1.0.0-alpha.1
+- name: OpenTelemetry.Resources.Host
+ description: OpenTelemetry Resource Detectors for Host.
+ type: extension
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Resources.OperatingSystem
+ description: OpenTelemetry Resource Detectors for Operating System.
+ type: extension
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Resources.Process
+ description: OpenTelemetry Resource Detectors for Process.
+ type: extension
+ version: 1.15.1-beta.2
+- name: OpenTelemetry.Resources.ProcessRuntime
+ description: OpenTelemetry Resource Detectors for Process Runtime.
+ type: extension
+ version: 1.15.1-beta.1
+- name: OpenTelemetry.Sampler.AWS
+ description: OpenTelemetry remote sampler for AWS X-Ray.
+ type: extension
+ version: 0.1.0-alpha.9
diff --git a/netlify.toml b/netlify.toml
index 8636e855..a071389f 100644
--- a/netlify.toml
+++ b/netlify.toml
@@ -15,9 +15,11 @@ command = "npm run netlify-build:production"
VITE_FARO_URL = "https://faro-collector-prod-us-east-2.grafana.net/collect/e587d17d38a90612e9dbedd3a3146b77"
[context.deploy-preview.environment]
+VITE_FEATURE_FLAG_JAVA_CONFIG_BUILDER = "true"
VITE_FEATURE_FLAG_COLLECTOR_PAGE = "true"
[context.branch-deploy.environment]
+VITE_FEATURE_FLAG_JAVA_CONFIG_BUILDER = "true"
VITE_FEATURE_FLAG_COLLECTOR_PAGE = "true"
[[edge_functions]]
diff --git a/pyproject.toml b/pyproject.toml
index ef4853bc..58c83bea 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -9,6 +9,7 @@ dependencies = [
"collector-watcher",
"configuration-watcher",
"java-instrumentation-watcher",
+ "dotnet-instrumentation-watcher",
"explorer-db-builder",
"v1-registry-sync",
]
@@ -22,6 +23,7 @@ members = [
collector-watcher = { workspace = true }
configuration-watcher = { workspace = true }
java-instrumentation-watcher = { workspace = true }
+dotnet-instrumentation-watcher = { workspace = true }
explorer-db-builder = { workspace = true }
v1-registry-sync = { workspace = true }
@@ -41,5 +43,6 @@ target-version = "py311"
select = ["E", "F", "I", "N", "W"]
[tool.pytest.ini_options]
+addopts = "--import-mode=importlib"
testpaths = ["ecosystem-automation"]
python_files = ["test_*.py", "*_test.py"]
\ No newline at end of file
diff --git a/uv.lock b/uv.lock
index 75acbe30..03d95711 100644
--- a/uv.lock
+++ b/uv.lock
@@ -6,9 +6,11 @@ requires-python = ">=3.11"
members = [
"collector-watcher",
"configuration-watcher",
+ "dotnet-instrumentation-watcher",
"explorer-db-builder",
"java-instrumentation-watcher",
"opentelemetry-ecosystem-explorer",
+ "v1-registry-sync",
"watcher-common",
]
@@ -428,6 +430,34 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
]
+[[package]]
+name = "dotnet-instrumentation-watcher"
+version = "0.1.0"
+source = { editable = "ecosystem-automation/dotnet-instrumentation-watcher" }
+dependencies = [
+ { name = "pyyaml" },
+ { name = "requests" },
+ { name = "semantic-version" },
+ { name = "watcher-common" },
+]
+
+[package.optional-dependencies]
+dev = [
+ { name = "pytest" },
+ { name = "pytest-cov" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
+ { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" },
+ { name = "pyyaml", specifier = ">=6.0.1" },
+ { name = "requests", specifier = ">=2.31.0" },
+ { name = "semantic-version", specifier = ">=2.10.0" },
+ { name = "watcher-common", editable = "ecosystem-automation/watcher-common" },
+]
+provides-extras = ["dev"]
+
[[package]]
name = "explorer-db-builder"
version = "0.1.0"
@@ -562,8 +592,10 @@ source = { virtual = "." }
dependencies = [
{ name = "collector-watcher" },
{ name = "configuration-watcher" },
+ { name = "dotnet-instrumentation-watcher" },
{ name = "explorer-db-builder" },
{ name = "java-instrumentation-watcher" },
+ { name = "v1-registry-sync" },
]
[package.dev-dependencies]
@@ -578,8 +610,10 @@ dev = [
requires-dist = [
{ name = "collector-watcher", editable = "ecosystem-automation/collector-watcher" },
{ name = "configuration-watcher", editable = "ecosystem-automation/configuration-watcher" },
+ { name = "dotnet-instrumentation-watcher", editable = "ecosystem-automation/dotnet-instrumentation-watcher" },
{ name = "explorer-db-builder", editable = "ecosystem-automation/explorer-db-builder" },
{ name = "java-instrumentation-watcher", editable = "ecosystem-automation/java-instrumentation-watcher" },
+ { name = "v1-registry-sync", editable = "ecosystem-automation/v1-registry-sync" },
]
[package.metadata.requires-dev]
@@ -944,6 +978,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
]
+[[package]]
+name = "v1-registry-sync"
+version = "0.1.0"
+source = { editable = "ecosystem-automation/v1-registry-sync" }
+dependencies = [
+ { name = "collector-watcher" },
+]
+
+[package.optional-dependencies]
+dev = [
+ { name = "pytest" },
+ { name = "pytest-cov" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "collector-watcher", editable = "ecosystem-automation/collector-watcher" },
+ { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
+ { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" },
+]
+provides-extras = ["dev"]
+
[[package]]
name = "virtualenv"
version = "21.2.4"