diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index f00b8d74..a83159c0 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -76,6 +76,11 @@ jobs: cd ecosystem-automation/explorer-db-builder uv run pytest tests/ --cov=explorer_db_builder --cov-report=term-missing --cov-report=json + - name: Run dotnet-instrumentation-watcher tests + run: | + cd ecosystem-automation/dotnet-instrumentation-watcher + uv run pytest tests/ --cov=dotnet_instrumentation_watcher --cov-report=term-missing --cov-report=json + test-ecosystem-explorer: runs-on: ubuntu-latest defaults: diff --git a/.github/workflows/nightly-registry-update.yml b/.github/workflows/nightly-registry-update.yml index 36bc4e97..2114932b 100644 --- a/.github/workflows/nightly-registry-update.yml +++ b/.github/workflows/nightly-registry-update.yml @@ -22,6 +22,7 @@ jobs: outputs: collector_result: ${{ steps.collector_watcher.outcome }} java_result: ${{ steps.java_instrumentation_watcher.outcome }} + dotnet_result: ${{ steps.dotnet_instrumentation_watcher.outcome }} configuration_result: ${{ steps.configuration_watcher.outcome }} steps: - name: Checkout code @@ -140,6 +141,12 @@ jobs: run: uv run java-instrumentation-watcher continue-on-error: true + - name: Run dotnet-instrumentation-watcher + id: dotnet_instrumentation_watcher + if: always() + run: uv run dotnet-instrumentation-watcher + continue-on-error: true + - name: Run configuration-watcher id: configuration_watcher if: always() @@ -286,3 +293,14 @@ jobs: with: success: ${{ needs.synchronize-inventory.outputs.configuration_result == 'success' }} watcher-name: "configuration-watcher" + + notify-dotnet: + permissions: + contents: read + issues: write + needs: [synchronize-inventory] + if: ${{ !cancelled() && needs.synchronize-inventory.result != 'skipped' }} + uses: ./.github/workflows/reusable-workflow-notification.yml + with: + success: ${{ needs.synchronize-inventory.outputs.dotnet_result == 'success' }} + watcher-name: "dotnet-instrumentation-watcher" diff --git a/ecosystem-automation/configuration-watcher/tests/__init__.py b/ecosystem-automation/configuration-watcher/tests/__init__.py deleted file mode 100644 index 131377bc..00000000 --- a/ecosystem-automation/configuration-watcher/tests/__init__.py +++ /dev/null @@ -1,14 +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. -# diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/pyproject.toml b/ecosystem-automation/dotnet-instrumentation-watcher/pyproject.toml new file mode 100644 index 00000000..be967d77 --- /dev/null +++ b/ecosystem-automation/dotnet-instrumentation-watcher/pyproject.toml @@ -0,0 +1,30 @@ +[project] +name = "dotnet-instrumentation-watcher" +version = "0.1.0" +description = "Automation tool for watching and collecting OpenTelemetry .NET instrumentation metadata" +requires-python = ">=3.11" +dependencies = [ + "PyYAML>=6.0.1", + "requests>=2.31.0", + "semantic-version>=2.10.0", + "watcher-common", +] + +[project.scripts] +dotnet-instrumentation-watcher = "dotnet_instrumentation_watcher.main:main" + +[project.optional-dependencies] +dev = [ + "pytest>=8.0.0", + "pytest-cov>=4.1.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.uv.sources] +watcher-common = { workspace = true } + +[tool.hatch.build.targets.wheel] +packages = ["src/dotnet_instrumentation_watcher"] diff --git a/ecosystem-automation/collector-watcher/tests/__init__.py b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/__init__.py similarity index 92% rename from ecosystem-automation/collector-watcher/tests/__init__.py rename to ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/__init__.py index 232bc78c..9645902c 100644 --- a/ecosystem-automation/collector-watcher/tests/__init__.py +++ b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/__init__.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -"""Tests for collector-watcher.""" +"""Dotnet Instrumentation Watcher package.""" diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/dotnet_client.py b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/dotnet_client.py new file mode 100644 index 00000000..bc753cf5 --- /dev/null +++ b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/dotnet_client.py @@ -0,0 +1,184 @@ +# 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. +# +"""NuGet API client for fetching .NET instrumentation data.""" + +import logging +from typing import Any, Dict, List + +import requests +from requests.adapters import HTTPAdapter +from urllib3 import Retry + +logger = logging.getLogger(__name__) + + +class NuGetAPIError(Exception): + """Custom exception for NuGet API errors.""" + + pass + + +class DotNetInstrumentationClient: + """Client for fetching .NET instrumentation metadata from NuGet.""" + + SERVICE_INDEX_URL = "https://api.nuget.org/v3/index.json" + OWNER = "OpenTelemetry" + TIMEOUT = 30 + + def __init__(self): + """Initialize the client.""" + self._session = requests.Session() + self._search_url = None + + retry_strategy = Retry( + total=3, + backoff_factor=1, + status_forcelist=[429, 500, 502, 503, 504], + ) + + adapter = HTTPAdapter(max_retries=retry_strategy) + self._session.mount("https://", adapter) + + def _get_search_url(self) -> str: + """Resolve the search URL from the NuGet service index. + + Raises: + NuGetAPIError: If the service index cannot be fetched or does not + contain a SearchQueryService resource. + """ + if self._search_url: + return self._search_url + + try: + response = self._session.get(self.SERVICE_INDEX_URL, timeout=self.TIMEOUT) + response.raise_for_status() + index_data = response.json() + except requests.RequestException as e: + raise NuGetAPIError(f"Error fetching NuGet service index: {e}") from e + + for resource in index_data.get("resources", []): + if resource.get("@type") == "SearchQueryService": + self._search_url = resource["@id"] + return self._search_url + + raise NuGetAPIError("NuGet service index did not contain a SearchQueryService resource") + + def fetch_instrumentation_list(self) -> Dict[str, Any]: + """ + Fetch instrumentation list by querying NuGet for packages owned by OpenTelemetry. + + The top-level ``version`` field in each search result entry is the latest + version of the package as reported by NuGet — no local sorting is needed. + """ + all_packages = self._fetch_all_packages_by_owner(self.OWNER) + modules = [] + + for pkg in all_packages: + package_id = pkg.get("id", "") + + # Skip packages flagged as deprecated by NuGet (includes Contrib packages). + if pkg.get("deprecation"): + logger.info(f" Skipping deprecated package: {package_id}") + continue + + # The top-level "version" field is the latest version returned by the + # NuGet search API — rely on the server ordering rather than sorting locally. + version = pkg.get("version", "") + description = pkg.get("description", "") + + # Filter and classify packages + if "Instrumentation" in package_id: + component_type = "instrumentation" + elif "Exporter" in package_id: + component_type = "exporter" + elif "Extensions" in package_id or "Resources" in package_id or "Sampler" in package_id: + component_type = "extension" + else: + # Skip core and unclassified packages. + continue + + modules.append( + { + "name": package_id, + "description": description or f"{package_id} for OpenTelemetry", + "type": component_type, + "version": version, + } + ) + + # Sort by name for deterministic registry output. + modules.sort(key=lambda x: x["name"]) + + return {"modules": modules} + + def get_core_version(self) -> str: + """Get the latest stable version of the core OpenTelemetry package. + + This is used as the 'ecosystem version' for the registry. + + Raises: + NuGetAPIError: If the version cannot be determined. + """ + params = { + "q": "PackageId:OpenTelemetry", + "prerelease": "false", + "semVerLevel": "2.0.0", + "take": 1, + } + try: + search_url = self._get_search_url() + response = self._session.get(search_url, params=params, timeout=self.TIMEOUT) + response.raise_for_status() + data = response.json() + results = data.get("data", []) + if not results: + raise NuGetAPIError("No results returned for core OpenTelemetry package") + return results[0]["version"] + except (KeyError, IndexError) as e: + raise NuGetAPIError(f"Unexpected response shape fetching core version: {e}") from e + except requests.RequestException as e: + raise NuGetAPIError(f"Error fetching core version: {e}") from e + + def _fetch_all_packages_by_owner(self, owner: str) -> List[Dict[str, Any]]: + """Fetch all packages for a specific owner using pagination.""" + packages = [] + skip = 0 + take = 20 + + while True: + params = { + "q": f"owner:{owner}", + "prerelease": "true", + "semVerLevel": "2.0.0", + "skip": skip, + "take": take, + } + try: + search_url = self._get_search_url() + response = self._session.get(search_url, params=params, timeout=self.TIMEOUT) + response.raise_for_status() + data = response.json() + + batch = data.get("data", []) + packages.extend(batch) + + if len(batch) < take: + break + + skip += take + except requests.RequestException as e: + raise NuGetAPIError(f"Error fetching packages from NuGet: {e}") from e + + return packages diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/instrumentation_sync.py b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/instrumentation_sync.py new file mode 100644 index 00000000..1c3f72ea --- /dev/null +++ b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/instrumentation_sync.py @@ -0,0 +1,142 @@ +# 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. +# +"""Synchronization orchestration for .NET instrumentation metadata.""" + +import logging +from typing import Any + +from semantic_version import Version + +from .dotnet_client import DotNetInstrumentationClient +from .inventory_manager import InventoryManager + +logger = logging.getLogger(__name__) + + +class InstrumentationSync: + """Orchestrates synchronization of .NET instrumentation metadata.""" + + def __init__( + self, + client: DotNetInstrumentationClient, + inventory_manager: InventoryManager, + ): + """ + Args: + client: GitHub API client for fetching data + inventory_manager: Inventory manager for storing data + """ + self.client = client + self.inventory_manager = inventory_manager + + def sync(self) -> dict[str, Any]: + """ + Synchronize .NET instrumentation metadata. + + This will: + 1. Process the latest release (if new) + 2. Update the snapshot from main branch + + Returns: + Summary dictionary with processing results + """ + summary = { + "new_release": None, + "snapshot_updated": None, + } + + logger.info("Checking for latest release...") + new_release = self.process_latest_release() + if new_release: + summary["new_release"] = str(new_release) + logger.info(f"[*] Processed new release: {new_release}") + else: + logger.info("[*] Latest release already tracked") + + logger.info("Updating snapshot from main branch...") + snapshot_version = self.update_snapshot() + summary["snapshot_updated"] = str(snapshot_version) + logger.info(f"[*] Updated snapshot: {snapshot_version}") + + return summary + + def process_latest_release(self) -> Version | None: + """ + Process the latest release if not already tracked. + + Returns: + Version if newly processed, None if already exists + """ + version_string = self.client.get_core_version() + logger.info(f" Latest core package version: {version_string}") + + try: + version = Version(version_string) + except ValueError as e: + raise ValueError(f"Invalid core version string: {version_string!r}") from e + + if self.inventory_manager.version_exists(version): + return None + + logger.info(f" Fetching instrumentation list for version {version_string}...") + instrumentations = self.client.fetch_instrumentation_list() + + self.inventory_manager.save_versioned_inventory( + version=version, + instrumentations=instrumentations, + ) + + return version + + def update_snapshot(self) -> Version: + """ + Update snapshot version from NuGet data. + + This will: + 1. Determine next snapshot version + 2. Fetch from NuGet + 3. Clean up old snapshots + 4. Save new snapshot + + Returns: + The snapshot version + """ + latest_version_string = self.client.get_core_version() + try: + latest_version = Version(latest_version_string) + except ValueError as e: + raise ValueError(f"Could not resolve a valid core version: {latest_version_string!r}") from e + + snapshot_version = Version( + major=latest_version.major, + minor=latest_version.minor, + patch=latest_version.patch, + prerelease=latest_version.prerelease, + build=("SNAPSHOT",), + ) + + logger.info(" Fetching instrumentation list from NuGet...") + instrumentations = self.client.fetch_instrumentation_list() + + removed = self.inventory_manager.cleanup_snapshots() + if removed > 0: + logger.info(f" Removed {removed} old snapshot(s)") + + self.inventory_manager.save_versioned_inventory( + version=snapshot_version, + instrumentations=instrumentations, + ) + + return snapshot_version diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/inventory_manager.py b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/inventory_manager.py new file mode 100644 index 00000000..2500eb06 --- /dev/null +++ b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/inventory_manager.py @@ -0,0 +1,90 @@ +# 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. +# +"""Inventory management for .NET instrumentation tracking.""" + +from typing import Any + +import yaml +from semantic_version import Version +from watcher_common.inventory_manager import BaseInventoryManager + + +class InventoryManager(BaseInventoryManager): + """Manages .NET instrumentation inventory storage and retrieval.""" + + FILE_NAME = "instrumentation.yaml" + + def __init__(self, inventory_dir: str = "ecosystem-registry/dotnet"): + """ + Args: + inventory_dir: Base directory for versioned metadata + """ + super().__init__(inventory_dir) + + def version_exists(self, version: Version) -> bool: + """ + Check if a specific version exists. + + Args: + version: Version to check + + Returns: + True if version directory and instrumentation file exist + """ + version_dir = self.get_version_dir(version) + return version_dir.exists() and (version_dir / self.FILE_NAME).exists() + + def save_versioned_inventory(self, version: Version, instrumentations: dict[str, Any]) -> None: + """ + Save inventory for a specific version. + + Args: + version: Version object + instrumentations: Instrumentation data dict + """ + version_dir = self.get_version_dir(version) + version_dir.mkdir(parents=True, exist_ok=True) + + file_path = version_dir / self.FILE_NAME + + inventory_data = { + **instrumentations, + } + + with open(file_path, "w") as f: + yaml.dump(inventory_data, f, default_flow_style=False, sort_keys=False, allow_unicode=True) + + def load_versioned_inventory(self, version: Version) -> dict[str, Any]: + """ + Load inventory for a specific version. + + Args: + version: Version object + + Returns: + Inventory dictionary with full structure, or empty structure if it doesn't exist + """ + version_dir = self.get_version_dir(version) + file_path = version_dir / self.FILE_NAME + + if not file_path.exists(): + return { + "file_format": 0.1, + "libraries": [], + } + + with open(file_path) as f: + data = yaml.safe_load(f) or {} + return data diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/main.py b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/main.py new file mode 100644 index 00000000..c38d0b33 --- /dev/null +++ b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/main.py @@ -0,0 +1,82 @@ +# 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. +# +"""Main entry point for dotnet instrumentation watcher.""" + +import argparse +import logging +import sys + +from .dotnet_client import DotNetInstrumentationClient +from .instrumentation_sync import InstrumentationSync +from .inventory_manager import InventoryManager + +logger = logging.getLogger(__name__) + + +def configure_logging(): + """Configure logging to output to stdout.""" + logging.basicConfig( + level=logging.INFO, + format="%(message)s", + handlers=[logging.StreamHandler(sys.stdout)], + ) + + +def main(): + """Synchronize dotnet instrumentation metadata to the registry.""" + configure_logging() + + parser = argparse.ArgumentParser( + description="Synchronize .NET instrumentation metadata to the registry", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument( + "--inventory-dir", + default="ecosystem-registry/dotnet", + help="Directory path for the inventory", + ) + args = parser.parse_args() + + logger.info("=" * 60) + logger.info(".NET Instrumentation Watcher") + logger.info("=" * 60) + logger.info(f"Inventory directory: {args.inventory_dir}") + logger.info("") + + try: + client = DotNetInstrumentationClient() + inventory_manager = InventoryManager(inventory_dir=args.inventory_dir) + + sync = InstrumentationSync(client, inventory_manager) + summary = sync.sync() + + logger.info("") + logger.info("=" * 60) + logger.info("Sync Summary") + logger.info("=" * 60) + if summary["new_release"]: + logger.info(f"[*] New release processed: {summary['new_release']}") + else: + logger.info("[*] No new releases") + logger.info(f"[*] Snapshot updated: {summary['snapshot_updated']}") + logger.info("") + + except Exception as e: + logger.exception(f"Failed to sync: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_dotnet_client.py b/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_dotnet_client.py new file mode 100644 index 00000000..45d49884 --- /dev/null +++ b/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_dotnet_client.py @@ -0,0 +1,126 @@ +# 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. +# +from unittest.mock import MagicMock, patch + +import pytest +from dotnet_instrumentation_watcher.dotnet_client import ( + DotNetInstrumentationClient, + NuGetAPIError, +) +from requests import RequestException + + +def _make_index_response(): + """Return a mock NuGet service index response.""" + mock = MagicMock() + mock.json.return_value = {"resources": [{"@id": "https://api.test/query", "@type": "SearchQueryService"}]} + return mock + + +def test_get_core_version_success(): + client = DotNetInstrumentationClient() + with patch.object(client._session, "get") as mock_get: + mock_get.side_effect = [ + _make_index_response(), + MagicMock(**{"json.return_value": {"data": [{"version": "1.15.3"}]}}), + ] + + version = client.get_core_version() + assert version == "1.15.3" + assert mock_get.call_count == 2 + + +def test_get_core_version_raises_on_api_error(): + """get_core_version should propagate NuGetAPIError instead of returning a fallback.""" + client = DotNetInstrumentationClient() + with patch.object(client._session, "get") as mock_get: + mock_get.side_effect = RequestException("API error") + with pytest.raises(NuGetAPIError): + client.get_core_version() + + +def test_get_search_url_raises_when_service_index_missing_resource(): + """_get_search_url should raise NuGetAPIError if no SearchQueryService is in the index.""" + client = DotNetInstrumentationClient() + with patch.object(client._session, "get") as mock_get: + mock_resp = MagicMock() + mock_resp.json.return_value = {"resources": []} # no SearchQueryService entry + mock_get.return_value = mock_resp + with pytest.raises(NuGetAPIError, match="SearchQueryService"): + client._get_search_url() + + +def test_fetch_instrumentation_list(): + """Packages are filtered by deprecation flag only; prerelease versions are supported.""" + client = DotNetInstrumentationClient() + with patch.object(client._session, "get") as mock_get: + mock_search_response = MagicMock() + mock_search_response.json.return_value = { + "data": [ + # Stable instrumentation + { + "id": "OpenTelemetry.Instrumentation.Test", + "version": "1.0.0", + "description": "Test instrumentation", + }, + # Stable exporter + { + "id": "OpenTelemetry.Exporter.Test", + "version": "1.1.0", + "description": "Test exporter", + }, + # Extension with a prerelease version (e.g. 1.2.3-preview.4) + { + "id": "OpenTelemetry.Extensions.Test", + "version": "1.2.3-preview.4", + "description": "Test extension", + }, + # Deprecated by NuGet — must be skipped + { + "id": "OpenTelemetry.Exporter.Deprecated", + "version": "0.1.0", + "deprecation": {"reasons": ["Legacy"]}, + }, + # Contrib package — deprecated in NuGet, so covered by the deprecation check + { + "id": "OpenTelemetry.Contrib.Instrumentation.Legacy", + "version": "1.0.0", + "description": "Legacy contrib package", + "deprecation": {"reasons": ["Legacy"]}, + }, + ] + } + mock_get.side_effect = [_make_index_response(), mock_search_response] + + result = client.fetch_instrumentation_list() + assert "modules" in result + # 3 valid packages (1 deprecated + 1 deprecated contrib are skipped) + assert len(result["modules"]) == 3 + + types = [m["type"] for m in result["modules"]] + assert "instrumentation" in types + assert "exporter" in types + assert "extension" in types + + # Sorted by name: Exporter.Test, Extensions.Test, Instrumentation.Test + assert result["modules"][0]["name"] == "OpenTelemetry.Exporter.Test" + assert result["modules"][0]["version"] == "1.1.0" + + # Extension carries a prerelease version — must be preserved as-is + assert result["modules"][1]["name"] == "OpenTelemetry.Extensions.Test" + assert result["modules"][1]["version"] == "1.2.3-preview.4" + + assert result["modules"][2]["name"] == "OpenTelemetry.Instrumentation.Test" + assert result["modules"][2]["version"] == "1.0.0" diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_instrumentation_sync.py b/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_instrumentation_sync.py new file mode 100644 index 00000000..cdf918cd --- /dev/null +++ b/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_instrumentation_sync.py @@ -0,0 +1,67 @@ +# 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. +# +from unittest.mock import MagicMock + +import pytest +from dotnet_instrumentation_watcher.instrumentation_sync import InstrumentationSync +from semantic_version import Version + + +@pytest.fixture +def mock_client(): + client = MagicMock() + return client + + +@pytest.fixture +def mock_inventory(): + inventory = MagicMock() + return inventory + + +def test_process_latest_release(mock_client, mock_inventory): + mock_client.get_core_version.return_value = "1.2.3" + mock_inventory.version_exists.return_value = False + mock_client.fetch_instrumentation_list.return_value = {"modules": []} + + sync = InstrumentationSync(mock_client, mock_inventory) + result = sync.process_latest_release() + + assert result == Version("1.2.3") + mock_inventory.save_versioned_inventory.assert_called_once() + + +def test_process_latest_release_already_exists(mock_client, mock_inventory): + mock_client.get_core_version.return_value = "1.2.3" + mock_inventory.version_exists.return_value = True + + sync = InstrumentationSync(mock_client, mock_inventory) + result = sync.process_latest_release() + + assert result is None + mock_inventory.save_versioned_inventory.assert_not_called() + + +def test_update_snapshot(mock_client, mock_inventory): + mock_client.get_core_version.return_value = "1.2.3" + mock_client.fetch_instrumentation_list.return_value = {"modules": []} + mock_inventory.cleanup_snapshots.return_value = 1 + + sync = InstrumentationSync(mock_client, mock_inventory) + result = sync.update_snapshot() + + assert result == Version("1.2.3+SNAPSHOT") + mock_inventory.cleanup_snapshots.assert_called_once() + mock_inventory.save_versioned_inventory.assert_called_once() diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_inventory_manager.py b/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_inventory_manager.py new file mode 100644 index 00000000..0024ee9a --- /dev/null +++ b/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_inventory_manager.py @@ -0,0 +1,57 @@ +# 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 os +import tempfile + +import pytest +from dotnet_instrumentation_watcher.inventory_manager import InventoryManager +from semantic_version import Version + + +@pytest.fixture +def temp_workspace(): + with tempfile.TemporaryDirectory() as temp_dir: + # Create mock workspace + registry_dir = os.path.join(temp_dir, "ecosystem-registry", "dotnet") + os.makedirs(registry_dir) + yield temp_dir + + +def test_has_version(temp_workspace): + manager = InventoryManager(inventory_dir=temp_workspace) + assert manager.version_exists(Version("1.0.0")) is False + + +def test_save_and_list_versions(temp_workspace): + manager = InventoryManager(inventory_dir=temp_workspace) + + mock_data = {"modules": [{"name": "test"}]} + manager.save_versioned_inventory(Version("1.0.0"), mock_data) + + assert manager.version_exists(Version("1.0.0")) is True + versions = manager.list_versions() + assert Version("1.0.0") in versions + + +def test_cleanup_snapshots(temp_workspace): + manager = InventoryManager(inventory_dir=temp_workspace) + + mock_data = {"modules": []} + manager.save_versioned_inventory(Version("1.0.0-SNAPSHOT"), mock_data) + manager.save_versioned_inventory(Version("1.0.1-SNAPSHOT"), mock_data) + + manager.cleanup_snapshots() + assert not manager.version_exists(Version("1.0.0-SNAPSHOT")) + assert not manager.version_exists(Version("1.0.1-SNAPSHOT")) diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_main.py b/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_main.py new file mode 100644 index 00000000..5db1a84b --- /dev/null +++ b/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_main.py @@ -0,0 +1,42 @@ +# 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. +# +from unittest.mock import patch + +import pytest +from dotnet_instrumentation_watcher.main import main + + +@patch("dotnet_instrumentation_watcher.main.DotNetInstrumentationClient") +@patch("dotnet_instrumentation_watcher.main.InventoryManager") +@patch("dotnet_instrumentation_watcher.main.InstrumentationSync") +def test_main_success(mock_sync, mock_inventory, mock_client): + mock_sync_instance = mock_sync.return_value + mock_sync_instance.sync.return_value = {"new_release": "1.0.0", "snapshot_updated": "1.0.1-SNAPSHOT"} + + with patch("sys.argv", ["dotnet-instrumentation-watcher"]): + assert main() is None + + +@patch("dotnet_instrumentation_watcher.main.DotNetInstrumentationClient") +@patch("dotnet_instrumentation_watcher.main.InventoryManager") +@patch("dotnet_instrumentation_watcher.main.InstrumentationSync") +def test_main_failure(mock_sync, mock_inventory, mock_client): + mock_sync_instance = mock_sync.return_value + mock_sync_instance.sync.side_effect = Exception("Test error") + + with patch("sys.argv", ["dotnet-instrumentation-watcher"]): + with pytest.raises(SystemExit) as excinfo: + main() + assert excinfo.value.code == 1 diff --git a/ecosystem-automation/explorer-db-builder/src/explorer_db_builder/instrumentation_transformer.py b/ecosystem-automation/explorer-db-builder/src/explorer_db_builder/instrumentation_transformer.py index 21402367..eed34e38 100644 --- a/ecosystem-automation/explorer-db-builder/src/explorer_db_builder/instrumentation_transformer.py +++ b/ecosystem-automation/explorer-db-builder/src/explorer_db_builder/instrumentation_transformer.py @@ -91,7 +91,7 @@ def _transform_0_1_to_0_2(inventory_data: dict[str, Any]) -> dict[str, Any]: Returns: Transformed inventory data in format 0.2 """ - if "libraries" not in inventory_data: + if "libraries" not in inventory_data or inventory_data["libraries"] is None: raise KeyError("Inventory data missing 'libraries' key") transformed_data = inventory_data.copy() @@ -143,6 +143,7 @@ def _transform_0_2_to_0_3(inventory_data: dict[str, Any]) -> dict[str, Any]: Returns: Transformed inventory data in format 0.3 """ + transformed_data = inventory_data.copy() if inventory_data.get("libraries") is not None: diff --git a/ecosystem-automation/explorer-db-builder/src/explorer_db_builder/main.py b/ecosystem-automation/explorer-db-builder/src/explorer_db_builder/main.py index f8aba690..43472143 100644 --- a/ecosystem-automation/explorer-db-builder/src/explorer_db_builder/main.py +++ b/ecosystem-automation/explorer-db-builder/src/explorer_db_builder/main.py @@ -188,7 +188,7 @@ def load_and_augment_inventory(version: Version) -> dict: logger.info(f" Files written: {stats['files_written']}") logger.info(f" Total size: {stats['total_bytes']:,} bytes ({total_mb:.2f} MB)") logger.info("") - logger.info("✓ Database build completed successfully") + logger.info("[*] Database build completed successfully") return 0 except ValueError as e: diff --git a/ecosystem-automation/explorer-db-builder/tests/__init__.py b/ecosystem-automation/explorer-db-builder/tests/__init__.py deleted file mode 100644 index 145bf306..00000000 --- a/ecosystem-automation/explorer-db-builder/tests/__init__.py +++ /dev/null @@ -1,15 +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. -# -"""Tests for explorer-db-builder.""" diff --git a/ecosystem-explorer/.env.development b/ecosystem-explorer/.env.development index e2bcd887..323fd815 100644 --- a/ecosystem-explorer/.env.development +++ b/ecosystem-explorer/.env.development @@ -1,2 +1,3 @@ +VITE_FEATURE_FLAG_JAVA_CONFIG_BUILDER=true VITE_FEATURE_FLAG_COLLECTOR_PAGE=true VITE_FEATURE_FLAG_V1_REDESIGN=false \ No newline at end of file diff --git a/ecosystem-explorer/.env.test b/ecosystem-explorer/.env.test index e2bcd887..323fd815 100644 --- a/ecosystem-explorer/.env.test +++ b/ecosystem-explorer/.env.test @@ -1,2 +1,3 @@ +VITE_FEATURE_FLAG_JAVA_CONFIG_BUILDER=true VITE_FEATURE_FLAG_COLLECTOR_PAGE=true VITE_FEATURE_FLAG_V1_REDESIGN=false \ No newline at end of file diff --git a/ecosystem-explorer/README.md b/ecosystem-explorer/README.md index 560533a5..a0f067b5 100644 --- a/ecosystem-explorer/README.md +++ b/ecosystem-explorer/README.md @@ -90,11 +90,11 @@ prefixed with `VITE_FEATURE_FLAG_`. They are evaluated at build time. Update `.env.development` file and set the flag to `true`, `1`, or `yes`: ```bash -VITE_FEATURE_FLAG_COLLECTOR_PAGE=true +VITE_FEATURE_FLAG_JAVA_CONFIG_BUILDER=true ``` -For example, setting `VITE_FEATURE_FLAG_COLLECTOR_PAGE` to `true` makes the Collector Page visible, -while setting it to `false` hides it. +For example, setting `VITE_FEATURE_FLAG_JAVA_CONFIG_BUILDER` to `true` makes the Java Config Builder +visible, while setting it to `false` hides it. **Using a flag in code:** @@ -102,7 +102,7 @@ while setting it to `false` hides it. import { isEnabled } from "@/lib/feature-flags"; { - isEnabled("COLLECTOR_PAGE") && ; + isEnabled("JAVA_CONFIG_BUILDER") && ; } ``` @@ -110,10 +110,10 @@ The available feature flags are defined in `src/lib/feature-flags.ts`. **Deployment behavior:** -Branch deploys and deploy previews enable `COLLECTOR_PAGE` through `netlify.toml`. -`JAVA_RELEASE_COMPARISON` is not enabled in any deploy context. `V1_REDESIGN` is enabled -automatically on `feat/84-*` branches via the build command. Production enables none of these flags -by default. +Branch deploys and deploy previews enable `JAVA_CONFIG_BUILDER` and `COLLECTOR_PAGE` through +`netlify.toml`. `JAVA_RELEASE_COMPARISON` is not enabled in any deploy context. `V1_REDESIGN` is +enabled automatically on `feat/84-*` branches via the build command. Production enables none of +these flags by default. ## Data Fetching and Caching diff --git a/ecosystem-explorer/bun.lock b/ecosystem-explorer/bun.lock index 9cdbd993..8bf26691 100644 --- a/ecosystem-explorer/bun.lock +++ b/ecosystem-explorer/bun.lock @@ -24,15 +24,15 @@ }, "devDependencies": { "@eslint/js": "10.0.1", - "@tailwindcss/postcss": "4.3.0", + "@tailwindcss/postcss": "4.2.4", "@testing-library/jest-dom": "6.9.1", "@testing-library/react": "16.3.2", "@testing-library/user-event": "14.6.1", "@types/js-yaml": "4.0.9", - "@types/node": "25.8.0", + "@types/node": "25.6.2", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", - "@vitejs/plugin-react-swc": "4.3.1", + "@vitejs/plugin-react-swc": "4.3.0", "autoprefixer": "10.5.0", "eslint": "10.3.0", "eslint-config-prettier": "10.1.8", @@ -41,16 +41,16 @@ "fake-indexeddb": "6.2.5", "globals": "17.6.0", "jsdom": "29.1.1", - "playwright": "1.60.0", + "playwright": "1.59.1", "postcss": "8.5.14", "prettier": "3.8.3", "prettier-plugin-tailwindcss": "0.8.0", - "tailwindcss": "4.3.0", + "tailwindcss": "4.2.4", "typescript": "6.0.3", - "typescript-eslint": "8.59.3", + "typescript-eslint": "8.59.2", "typescript-json-schema": "0.67.2", - "vite": "8.0.13", - "vitest": "4.1.6", + "vite": "8.0.11", + "vitest": "4.1.5", }, }, }, @@ -212,7 +212,7 @@ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="], - "@oxc-project/types": ["@oxc-project/types@0.130.0", "", {}, "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q=="], + "@oxc-project/types": ["@oxc-project/types@0.128.0", "", {}, "sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ=="], "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], @@ -298,37 +298,37 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.1", "", { "os": "android", "cpu": "arm64" }, "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.18", "", { "os": "android", "cpu": "arm64" }, "sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ=="], - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ=="], - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g=="], - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw=="], - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.1", "", { "os": "linux", "cpu": "arm" }, "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18", "", { "os": "linux", "cpu": "arm" }, "sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg=="], - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ=="], - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug=="], - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg=="], - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18", "", { "os": "linux", "cpu": "s390x" }, "sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA=="], - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.1", "", { "os": "linux", "cpu": "x64" }, "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.18", "", { "os": "linux", "cpu": "x64" }, "sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw=="], - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.1", "", { "os": "linux", "cpu": "x64" }, "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.18", "", { "os": "linux", "cpu": "x64" }, "sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA=="], - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.1", "", { "os": "none", "cpu": "arm64" }, "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.18", "", { "os": "none", "cpu": "arm64" }, "sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A=="], - "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.1", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ=="], + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.18", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg=="], - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ=="], - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.1", "", { "os": "win32", "cpu": "x64" }, "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.18", "", { "os": "win32", "cpu": "x64" }, "sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="], "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], @@ -362,35 +362,35 @@ "@swc/types": ["@swc/types@0.1.26", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw=="], - "@tailwindcss/node": ["@tailwindcss/node@4.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.0" } }, "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g=="], + "@tailwindcss/node": ["@tailwindcss/node@4.2.4", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.4" } }, "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA=="], - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.0", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.0", "@tailwindcss/oxide-darwin-arm64": "4.3.0", "@tailwindcss/oxide-darwin-x64": "4.3.0", "@tailwindcss/oxide-freebsd-x64": "4.3.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", "@tailwindcss/oxide-linux-x64-musl": "4.3.0", "@tailwindcss/oxide-wasm32-wasi": "4.3.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg=="], + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.4", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.4", "@tailwindcss/oxide-darwin-arm64": "4.2.4", "@tailwindcss/oxide-darwin-x64": "4.2.4", "@tailwindcss/oxide-freebsd-x64": "4.2.4", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", "@tailwindcss/oxide-linux-x64-musl": "4.2.4", "@tailwindcss/oxide-wasm32-wasi": "4.2.4", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" } }, "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q=="], - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng=="], + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.4", "", { "os": "android", "cpu": "arm64" }, "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g=="], - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ=="], + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg=="], - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA=="], + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg=="], - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ=="], + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw=="], - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0", "", { "os": "linux", "cpu": "arm" }, "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA=="], + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA=="], - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg=="], + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw=="], - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ=="], + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g=="], - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ=="], + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA=="], - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg=="], + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA=="], - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.0", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA=="], + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.4", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw=="], - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ=="], + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ=="], - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="], + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw=="], - "@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.0", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "postcss": "^8.5.10", "tailwindcss": "4.3.0" } }, "sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w=="], + "@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.4", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.4", "@tailwindcss/oxide": "4.2.4", "postcss": "^8.5.6", "tailwindcss": "4.2.4" } }, "sha512-wgAVj6nUWAolAu8YFvzT2cTBIElWHkjZwFYovF+xsqKsW2ADxM/X2opxj5NsF/qVccAOjRNe8X2IdPzMsWyHTg=="], "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], @@ -434,7 +434,7 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@25.8.0", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ=="], + "@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], @@ -442,43 +442,43 @@ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.3", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/type-utils": "8.59.3", "@typescript-eslint/utils": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.3", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/type-utils": "8.59.2", "@typescript-eslint/utils": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.3", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.3", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.3", "@typescript-eslint/types": "^8.59.3", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.2", "@typescript-eslint/types": "^8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.3", "", { "dependencies": { "@typescript-eslint/types": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3" } }, "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2" } }, "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.3", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.3", "", { "dependencies": { "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3", "@typescript-eslint/utils": "8.59.3", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.59.3", "", {}, "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.59.2", "", {}, "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.3", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.3", "@typescript-eslint/tsconfig-utils": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.2", "@typescript-eslint/tsconfig-utils": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.3", "", { "dependencies": { "@typescript-eslint/types": "8.59.3", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="], - "@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@4.3.1", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.0", "@swc/core": "^1.15.11" }, "peerDependencies": { "vite": "^4 || ^5 || ^6 || ^7 || ^8" } }, "sha512-PaeokKjAGraNN+s5SIApgsktnJprIyt3zgEIu7awnEdfn29QiB2crTcCzyi2XGpX9rUnTc0cKU07Wm0N0g7H2w=="], + "@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@4.3.0", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7", "@swc/core": "^1.15.11" }, "peerDependencies": { "vite": "^4 || ^5 || ^6 || ^7 || ^8" } }, "sha512-mOkXCII839dHyAt/gpoSlm28JIVDwhZ6tnG6wJxUy2bmOx7UaPjvOyIDf3SFv5s7Eo7HVaq6kRcu6YMEzt5Z7w=="], - "@vitest/expect": ["@vitest/expect@4.1.6", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.6", "@vitest/utils": "4.1.6", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg=="], + "@vitest/expect": ["@vitest/expect@4.1.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.5", "@vitest/utils": "4.1.5", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw=="], - "@vitest/mocker": ["@vitest/mocker@4.1.6", "", { "dependencies": { "@vitest/spy": "4.1.6", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ=="], + "@vitest/mocker": ["@vitest/mocker@4.1.5", "", { "dependencies": { "@vitest/spy": "4.1.5", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw=="], - "@vitest/pretty-format": ["@vitest/pretty-format@4.1.6", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw=="], + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.5", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g=="], - "@vitest/runner": ["@vitest/runner@4.1.6", "", { "dependencies": { "@vitest/utils": "4.1.6", "pathe": "^2.0.3" } }, "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA=="], + "@vitest/runner": ["@vitest/runner@4.1.5", "", { "dependencies": { "@vitest/utils": "4.1.5", "pathe": "^2.0.3" } }, "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ=="], - "@vitest/snapshot": ["@vitest/snapshot@4.1.6", "", { "dependencies": { "@vitest/pretty-format": "4.1.6", "@vitest/utils": "4.1.6", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw=="], + "@vitest/snapshot": ["@vitest/snapshot@4.1.5", "", { "dependencies": { "@vitest/pretty-format": "4.1.5", "@vitest/utils": "4.1.5", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ=="], - "@vitest/spy": ["@vitest/spy@4.1.6", "", {}, "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg=="], + "@vitest/spy": ["@vitest/spy@4.1.5", "", {}, "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ=="], - "@vitest/utils": ["@vitest/utils@4.1.6", "", { "dependencies": { "@vitest/pretty-format": "4.1.6", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ=="], + "@vitest/utils": ["@vitest/utils@4.1.5", "", { "dependencies": { "@vitest/pretty-format": "4.1.5", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug=="], "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], @@ -896,9 +896,9 @@ "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], - "playwright": ["playwright@1.60.0", "", { "dependencies": { "playwright-core": "1.60.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA=="], + "playwright": ["playwright@1.59.1", "", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="], - "playwright-core": ["playwright-core@1.60.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA=="], + "playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="], "postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="], @@ -950,7 +950,7 @@ "require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], - "rolldown": ["rolldown@1.0.1", "", { "dependencies": { "@oxc-project/types": "=0.130.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.1", "@rolldown/binding-darwin-arm64": "1.0.1", "@rolldown/binding-darwin-x64": "1.0.1", "@rolldown/binding-freebsd-x64": "1.0.1", "@rolldown/binding-linux-arm-gnueabihf": "1.0.1", "@rolldown/binding-linux-arm64-gnu": "1.0.1", "@rolldown/binding-linux-arm64-musl": "1.0.1", "@rolldown/binding-linux-ppc64-gnu": "1.0.1", "@rolldown/binding-linux-s390x-gnu": "1.0.1", "@rolldown/binding-linux-x64-gnu": "1.0.1", "@rolldown/binding-linux-x64-musl": "1.0.1", "@rolldown/binding-openharmony-arm64": "1.0.1", "@rolldown/binding-wasm32-wasi": "1.0.1", "@rolldown/binding-win32-arm64-msvc": "1.0.1", "@rolldown/binding-win32-x64-msvc": "1.0.1" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ=="], + "rolldown": ["rolldown@1.0.0-rc.18", "", { "dependencies": { "@oxc-project/types": "=0.128.0", "@rolldown/pluginutils": "1.0.0-rc.18" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.18", "@rolldown/binding-darwin-arm64": "1.0.0-rc.18", "@rolldown/binding-darwin-x64": "1.0.0-rc.18", "@rolldown/binding-freebsd-x64": "1.0.0-rc.18", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.18", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.18", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.18", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.18", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.18", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.18", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.18" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg=="], "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], @@ -990,7 +990,7 @@ "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], - "tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], + "tailwindcss": ["tailwindcss@4.2.4", "", {}, "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA=="], "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], @@ -1024,7 +1024,7 @@ "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], - "typescript-eslint": ["typescript-eslint@8.59.3", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.3", "@typescript-eslint/parser": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3", "@typescript-eslint/utils": "8.59.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg=="], + "typescript-eslint": ["typescript-eslint@8.59.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.2", "@typescript-eslint/parser": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ=="], "typescript-json-schema": ["typescript-json-schema@0.67.2", "", { "dependencies": { "@types/json-schema": "^7.0.15", "@types/node": "^24.10.2", "glob": "^13.0.6", "path-equal": "^1.2.5", "safe-stable-stringify": "^2.5.0", "ts-node": "^10.9.2", "typescript": "~5.9.3", "vm2": "^3.10.5", "yargs": "^18.0.0" }, "bin": { "typescript-json-schema": "bin/typescript-json-schema" } }, "sha512-QRMWWIRu9JF6gL2cQAC4J7k0iEPEjJRq/p1d2xRa8ZwfHP/tNV4p248PdKoRPKcWFro3v4SAsB23XV6HOl3tHQ=="], @@ -1032,7 +1032,7 @@ "undici": ["undici@7.25.0", "", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="], - "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], @@ -1060,9 +1060,9 @@ "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], - "vite": ["vite@8.0.13", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.14", "rolldown": "1.0.1", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw=="], + "vite": ["vite@8.0.11", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.14", "rolldown": "1.0.0-rc.18", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow=="], - "vitest": ["vitest@4.1.6", "", { "dependencies": { "@vitest/expect": "4.1.6", "@vitest/mocker": "4.1.6", "@vitest/pretty-format": "4.1.6", "@vitest/runner": "4.1.6", "@vitest/snapshot": "4.1.6", "@vitest/spy": "4.1.6", "@vitest/utils": "4.1.6", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.6", "@vitest/browser-preview": "4.1.6", "@vitest/browser-webdriverio": "4.1.6", "@vitest/coverage-istanbul": "4.1.6", "@vitest/coverage-v8": "4.1.6", "@vitest/ui": "4.1.6", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ=="], + "vitest": ["vitest@4.1.5", "", { "dependencies": { "@vitest/expect": "4.1.5", "@vitest/mocker": "4.1.5", "@vitest/pretty-format": "4.1.5", "@vitest/runner": "4.1.5", "@vitest/snapshot": "4.1.5", "@vitest/spy": "4.1.5", "@vitest/utils": "4.1.5", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.5", "@vitest/browser-preview": "4.1.5", "@vitest/browser-webdriverio": "4.1.5", "@vitest/coverage-istanbul": "4.1.5", "@vitest/coverage-v8": "4.1.5", "@vitest/ui": "4.1.5", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg=="], "vm2": ["vm2@3.11.3", "", { "dependencies": { "acorn": "^8.15.0", "acorn-walk": "^8.3.4" }, "bin": { "vm2": "bin/vm2" } }, "sha512-DO1TTKuOc+veL11VNOvJwRab80mghFKE40Av3bl6pdXs11bdiDMuR73owy+dS2EsTZEvRUeBkkBuDVRjV/RgEw=="], @@ -1138,7 +1138,7 @@ "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], - "protobufjs/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], + "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.18", "", {}, "sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw=="], "strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], @@ -1150,8 +1150,6 @@ "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - "protobufjs/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], - "typescript-json-schema/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], } } diff --git a/ecosystem-explorer/package.json b/ecosystem-explorer/package.json index b62a9f58..60c904dd 100644 --- a/ecosystem-explorer/package.json +++ b/ecosystem-explorer/package.json @@ -8,7 +8,7 @@ "esbuild" ], "scripts": { - "serve": "bun scripts/aggregate-configurations.mjs && vite", + "serve": "vite", "build": "bun scripts/aggregate-configurations.mjs && tsc -b && bun scripts/generate-schemas.mjs && vite build && bun scripts/generate-agent-docs.mjs", "generate-schemas": "bun scripts/generate-schemas.mjs", "generate-agent-docs": "bun scripts/generate-agent-docs.mjs", @@ -44,15 +44,15 @@ }, "devDependencies": { "@eslint/js": "10.0.1", - "@tailwindcss/postcss": "4.3.0", + "@tailwindcss/postcss": "4.2.4", "@testing-library/jest-dom": "6.9.1", "@testing-library/react": "16.3.2", "@testing-library/user-event": "14.6.1", "@types/js-yaml": "4.0.9", - "@types/node": "25.8.0", + "@types/node": "25.6.2", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", - "@vitejs/plugin-react-swc": "4.3.1", + "@vitejs/plugin-react-swc": "4.3.0", "autoprefixer": "10.5.0", "eslint": "10.3.0", "eslint-config-prettier": "10.1.8", @@ -61,15 +61,15 @@ "fake-indexeddb": "6.2.5", "globals": "17.6.0", "jsdom": "29.1.1", - "playwright": "1.60.0", + "playwright": "1.59.1", "postcss": "8.5.14", "prettier": "3.8.3", "prettier-plugin-tailwindcss": "0.8.0", - "tailwindcss": "4.3.0", + "tailwindcss": "4.2.4", "typescript": "6.0.3", - "typescript-eslint": "8.59.3", + "typescript-eslint": "8.59.2", "typescript-json-schema": "0.67.2", - "vite": "8.0.13", - "vitest": "4.1.6" + "vite": "8.0.11", + "vitest": "4.1.5" } } diff --git a/ecosystem-explorer/public/data/configuration/defaults/sdk-configuration-defaults-1.0.0.json b/ecosystem-explorer/public/data/configuration/defaults/sdk-configuration-defaults-1.0.0.json index 8d8f95a6..cc7c6b7f 100644 --- a/ecosystem-explorer/public/data/configuration/defaults/sdk-configuration-defaults-1.0.0.json +++ b/ecosystem-explorer/public/data/configuration/defaults/sdk-configuration-defaults-1.0.0.json @@ -1,69 +1,26 @@ { "enabledSections": { "resource": true, - "propagator": true, "tracer_provider": true, "meter_provider": true, - "logger_provider": true + "logger_provider": true, + "propagator": true }, "values": { "resource": { - "attributes_list": "${OTEL_RESOURCE_ATTRIBUTES}", - "detection/development": { - "detectors": [ - { "service": {} }, - { "host": {} }, - { "process": {} }, - { "container": {} } - ] - } - }, - "propagator": { - "composite": [{ "tracecontext": {} }, { "baggage": {} }] + "attributes": [{ "name": "service.name", "value": "unknown_service" }] }, "tracer_provider": { - "processors": [ - { - "batch": { - "exporter": { - "otlp_http": { - "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT:-http://localhost:4318}/v1/traces" - } - } - } - } - ], - "sampler": { - "parent_based": { - "root": { "always_on": {} } - } - } + "processors": [{ "batch": { "exporter": { "otlp_http": {} } } }] }, "meter_provider": { - "readers": [ - { - "periodic": { - "exporter": { - "otlp_http": { - "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT:-http://localhost:4318}/v1/metrics" - } - } - } - } - ] + "readers": [{ "periodic": { "exporter": { "otlp_http": {} } } }] }, "logger_provider": { - "processors": [ - { - "batch": { - "exporter": { - "otlp_http": { - "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT:-http://localhost:4318}/v1/logs" - } - } - } - } - ] + "processors": [{ "batch": { "exporter": { "otlp_http": {} } } }] + }, + "propagator": { + "composite": [{ "tracecontext": {} }, { "baggage": {} }] } } } diff --git a/ecosystem-explorer/public/data/javaagent/global-configurations.json b/ecosystem-explorer/public/data/javaagent/global-configurations.json deleted file mode 100644 index 88ea3eb3..00000000 --- a/ecosystem-explorer/public/data/javaagent/global-configurations.json +++ /dev/null @@ -1,1757 +0,0 @@ -[ - { - "default": false, - "description": "Enables experimental span attributes `job.system`, `scheduling.apache-elasticjob.job.name`, `scheduling.apache-elasticjob.task.id`, `scheduling.apache-elasticjob.sharding.item.index`, `scheduling.apache-elasticjob.sharding.total.count`, `scheduling.apache-elasticjob.sharding.item.parameter`, and `scheduling.apache-elasticjob.job.type`.", - "name": "otel.instrumentation.apache-elasticjob.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "apache-elasticjob-3.0" - ] - }, - { - "default": false, - "description": "Enables experimental `apache-shenyu.meta.` prefixed span attributes `app-name`, `service-name`, `context-path`, `param-types`, `id`, `method-name`, `rpc-type`, `path` and `rpc-ext`.", - "name": "otel.instrumentation.apache-shenyu.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "apache-shenyu-2.4" - ] - }, - { - "default": 10000, - "description": "Flush timeout in milliseconds.", - "name": "otel.instrumentation.aws-lambda.flush-timeout", - "type": "int", - "instrumentations": [ - "aws-lambda-events-3.11", - "aws-lambda-events-2.2", - "aws-lambda-core-1.0" - ] - }, - { - "declarative_name": "java.aws_sdk.record_individual_http_error/development", - "default": false, - "description": "Determines whether errors returned by each individual HTTP request should be recorded as events for the SDK span.", - "name": "otel.instrumentation.aws-sdk.experimental-record-individual-http-error", - "type": "boolean", - "instrumentations": [ - "aws-sdk-2.2" - ] - }, - { - "declarative_name": "java.aws_sdk.experimental_span_attributes/development", - "default": false, - "description": "Enables experimental span attributes `aws.agent`, `aws.lambda.function.arn` and `aws.lambda.function.name` for AWS SDK instrumentation.", - "name": "otel.instrumentation.aws-sdk.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "aws-sdk-2.2", - "aws-sdk-1.11" - ] - }, - { - "declarative_name": "java.aws_sdk.use_propagator_for_messaging/development", - "default": false, - "description": "Determines whether the configured TextMapPropagator should be used to inject into supported messaging attributes (for SQS).", - "name": "otel.instrumentation.aws-sdk.experimental-use-propagator-for-messaging", - "type": "boolean", - "instrumentations": [ - "aws-sdk-2.2" - ] - }, - { - "declarative_name": "java.camel.experimental_span_attributes/development", - "default": false, - "description": "Enable the capture of experimental `camel.uri`, `camel.kafka.partitionKey`, `camel.kafka.key` and `camel.kafka.offset` span attributes.", - "name": "otel.instrumentation.camel.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "camel-2.20" - ] - }, - { - "declarative_name": "java.cassandra.query_sanitization.enabled", - "default": true, - "description": "Enables query sanitization for Cassandra queries. Takes precedence over otel.instrumentation.common.db.query-sanitization.enabled.", - "name": "otel.instrumentation.cassandra.query-sanitization.enabled", - "type": "boolean", - "instrumentations": [ - "cassandra-3.0" - ] - }, - { - "default": true, - "description": "Enables statement sanitization for database queries.", - "name": "otel.instrumentation.common.db-statement-sanitizer.enabled", - "type": "boolean", - "instrumentations": [ - "cassandra-4.4", - "couchbase-2.6", - "clickhouse-client-v2-0.8", - "vertx-sql-client-4.0", - "vertx-redis-client-4.0", - "r2dbc-1.0", - "jedis-3.0", - "mongo-async-3.3", - "vertx-sql-client-5.0", - "mongo-3.7", - "couchbase-2.0", - "geode-1.4", - "jedis-4.0", - "cassandra-3.0", - "jdbc", - "mongo-4.0", - "jedis-1.4", - "cassandra-4.0", - "lettuce-5.0", - "clickhouse-client-v1-0.5", - "influxdb-2.4", - "lettuce-5.1", - "mongo-3.1" - ] - }, - { - "declarative_name": "java.common.db.query_sanitization.enabled", - "default": true, - "description": "Enables query sanitization for database queries.", - "name": "otel.instrumentation.common.db.query-sanitization.enabled", - "type": "boolean", - "instrumentations": [ - "cassandra-4.4", - "couchbase-2.6", - "clickhouse-client-v2-0.8", - "vertx-sql-client-4.0", - "vertx-redis-client-4.0", - "r2dbc-1.0", - "jedis-3.0", - "mongo-async-3.3", - "vertx-sql-client-5.0", - "mongo-3.7", - "couchbase-2.0", - "geode-1.4", - "jedis-4.0", - "cassandra-3.0", - "jdbc", - "mongo-4.0", - "jedis-1.4", - "cassandra-4.0", - "lettuce-5.0", - "clickhouse-client-v1-0.5", - "influxdb-2.4", - "lettuce-5.1", - "mongo-3.1" - ] - }, - { - "default": false, - "description": "Enables capturing the enduser.id attribute.", - "name": "otel.instrumentation.common.enduser.id.enabled", - "type": "boolean", - "instrumentations": [ - "spring-security-config-6.0" - ] - }, - { - "default": false, - "description": "Enables capturing the enduser.role attribute.", - "name": "otel.instrumentation.common.enduser.role.enabled", - "type": "boolean", - "instrumentations": [ - "spring-security-config-6.0" - ] - }, - { - "default": false, - "description": "Enables capturing the enduser.scope attribute.", - "name": "otel.instrumentation.common.enduser.scope.enabled", - "type": "boolean", - "instrumentations": [ - "spring-security-config-6.0" - ] - }, - { - "default": false, - "description": "Enables the creation of experimental controller spans.", - "name": "otel.instrumentation.common.experimental.controller-telemetry.enabled", - "type": "boolean", - "instrumentations": [ - "jfinal-3.2", - "jaxrs-3.0-resteasy-6.0", - "jaxws-2.0", - "spring-webmvc-6.0", - "jaxrs-2.0-resteasy-3.0", - "play-mvc-2.4", - "vaadin-14.2", - "ratpack-1.7", - "jaxrs-2.0-jersey-2.0", - "struts-2.3", - "finatra-2.9", - "jaxrs-3.0-jersey-3.0", - "tapestry-5.4", - "jsf-mojarra-1.2", - "jaxrs-2.0-cxf-3.2", - "jaxrs-1.0", - "jsf-myfaces-1.2", - "spring-ws-2.0", - "play-mvc-2.6", - "struts-7.0", - "jaxrs-2.0-annotations", - "jaxrs-3.0-annotations", - "jaxws-jws-api-1.1", - "jaxrs-2.0-resteasy-3.1", - "spring-webflux-5.0", - "jaxws-metro-2.2", - "spring-webmvc-3.1", - "ratpack-1.4", - "jaxws-cxf-3.0", - "jaxws-2.0-axis2-1.6", - "jsf-myfaces-3.0", - "jsf-mojarra-3.0", - "grails-3.0" - ], - "declarative_name": "java.common.controller_telemetry/development.enabled" - }, - { - "default": false, - "description": "Enables the creation of experimental view spans.", - "name": "otel.instrumentation.common.experimental.view-telemetry.enabled", - "type": "boolean", - "instrumentations": [ - "spring-webmvc-6.0", - "dropwizard-views-0.7", - "jsp-2.3", - "spring-webmvc-3.1" - ], - "declarative_name": "java.common.view_telemetry/development.enabled" - }, - { - "default": "span_id", - "description": "Specifies the key name used to store the span ID in the logging context.", - "name": "otel.instrumentation.common.logging.span-id", - "type": "string", - "instrumentations": [ - "log4j-mdc-1.2", - "logback-mdc-1.0", - "log4j-context-data-2.7", - "log4j-context-data-2.17" - ] - }, - { - "default": "trace_flags", - "description": "Specifies the key name used to store the trace flags in the logging context.", - "name": "otel.instrumentation.common.logging.trace-flags", - "type": "string", - "instrumentations": [ - "log4j-mdc-1.2", - "logback-mdc-1.0", - "log4j-context-data-2.7", - "log4j-context-data-2.17" - ] - }, - { - "default": "trace_id", - "description": "Specifies the key name used to store the trace ID in the logging context.", - "name": "otel.instrumentation.common.logging.trace-id", - "type": "string", - "instrumentations": [ - "log4j-mdc-1.2", - "logback-mdc-1.0", - "log4j-context-data-2.7", - "log4j-context-data-2.17" - ] - }, - { - "default": "", - "description": "Specifies which resource attributes to add to the logging context as a comma-separated list of attribute keys.", - "name": "otel.instrumentation.common.mdc.resource-attributes", - "type": "list", - "instrumentations": [ - "log4j-mdc-1.2", - "logback-mdc-1.0", - "log4j-context-data-2.7", - "log4j-context-data-2.17" - ] - }, - { - "default": "", - "description": "List of messaging headers to capture.", - "name": "otel.instrumentation.common.messaging.capture-headers", - "type": "list", - "instrumentations": [ - "rabbitmq-2.7" - ] - }, - { - "default": false, - "description": "Enables the creation of consumer spans on messaging receive operations. These spans will measure the time between receiving a message and the consumer processing that message.", - "name": "otel.instrumentation.common.messaging.experimental.receive-telemetry.enabled", - "type": "boolean", - "instrumentations": [ - "rabbitmq-2.7" - ] - }, - { - "default": "", - "description": "Used to specify a mapping from host names or IP addresses to peer services.", - "name": "otel.instrumentation.common.peer-service-mapping", - "type": "map", - "instrumentations": [ - "jetty-httpclient-12.0", - "async-http-client-1.9", - "apache-dubbo-2.7", - "jetty-httpclient-9.2", - "okhttp-2.2", - "reactor-netty-1.0", - "play-ws-1.0", - "vertx-redis-client-4.0", - "r2dbc-1.0", - "play-ws-2.1", - "jedis-3.0", - "pekko-http-1.0", - "apache-httpclient-2.0", - "async-http-client-1.8", - "ktor-3.0", - "java-http-client", - "vertx-http-client-3.0", - "http-url-connection", - "apache-httpasyncclient-4.1", - "vertx-http-client-4.0", - "ratpack-1.7", - "netty-4.1", - "vertx-http-client-5.0", - "armeria-1.3", - "jdbc", - "apache-httpclient-5.0", - "ktor-2.0", - "async-http-client-2.0", - "netty-3.8", - "akka-http-10.0", - "google-http-client-1.19", - "play-ws-2.0", - "netty-4.0", - "jedis-1.4", - "lettuce-5.0", - "apache-httpclient-4.0", - "jodd-http-4.2", - "lettuce-4.0", - "java-http-server", - "kubernetes-client-7.0", - "okhttp-3.0" - ] - }, - { - "declarative_name": "java.couchbase.experimental_span_attributes/development", - "default": false, - "description": "Enables experimental span attributes `couchbase.operation_id` and `couchbase.local.address`. Different operation types receive different experimental attributes.", - "name": "otel.instrumentation.couchbase.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "couchbase-2.6", - "couchbase-2.0" - ] - }, - { - "declarative_name": "java.dropwizard_metrics.enabled", - "default": false, - "description": "Enables the dropwizard metrics instrumentation.", - "name": "otel.instrumentation.dropwizard-metrics.enabled", - "type": "boolean", - "instrumentations": [ - "dropwizard-metrics-4.0" - ] - }, - { - "declarative_name": "java.elasticsearch.capture_search_query", - "default": false, - "description": "Enable the capture of search query bodies. It is important to note that Elasticsearch queries may contain personal or sensitive information.", - "name": "otel.instrumentation.elasticsearch.capture-search-query", - "type": "boolean", - "instrumentations": [ - "elasticsearch-rest-7.0", - "elasticsearch-rest-6.4", - "elasticsearch-rest-5.0" - ] - }, - { - "declarative_name": "java.elasticsearch.experimental_span_attributes/development", - "default": false, - "description": "Enable the capture of `elasticsearch.action`, `elasticsearch.id`, `elasticsearch.request`, `elasticsearch.request.indices`, `elasticsearch.request.write.type`, `elasticsearch.request.write.version`, `elasticsearch.response.status`, `elasticsearch.shard.replication.failed`, `elasticsearch.shard.replication.successful`, `elasticsearch.shard.replication.total`, `elasticsearch.type`, and `elasticsearch.version` experimental span attributes.", - "name": "otel.instrumentation.elasticsearch.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "elasticsearch-transport-6.0", - "elasticsearch-transport-5.3", - "elasticsearch-transport-5.0" - ] - }, - { - "declarative_name": "java.executors.include", - "default": "", - "description": "List of Executor subclasses to be instrumented.", - "examples": [ - "com.example.CustomExecutor", - "com.example.ExecutorOne,com.example.ExecutorTwo" - ], - "name": "otel.instrumentation.executors.include", - "type": "list", - "instrumentations": [ - "executors" - ] - }, - { - "declarative_name": "java.executors.include_all", - "default": false, - "description": "Whether to instrument all classes that implement the Executor interface.", - "name": "otel.instrumentation.executors.include-all", - "type": "boolean", - "instrumentations": [ - "executors" - ] - }, - { - "declarative_name": "java.external_annotations.exclude_methods", - "default": "", - "description": "All methods to be excluded from auto-instrumentation by annotation-based advices.", - "examples": [ - "com.example.MyClass[method1,method2]", - "com.example.MyClass[method1];com.example.OtherClass[method2]" - ], - "name": "otel.instrumentation.external-annotations.exclude-methods", - "type": "string", - "instrumentations": [ - "external-annotations" - ] - }, - { - "declarative_name": "java.external_annotations.include", - "default": "", - "description": "Configuration for trace annotations, in the form of a pattern that matches `'package.Annotation$Name;*'`.", - "examples": [ - "com.example.Trace", - "com.example.Trace;com.example.OtherTrace" - ], - "name": "otel.instrumentation.external-annotations.include", - "type": "string", - "instrumentations": [ - "external-annotations" - ] - }, - { - "declarative_name": "java.common.gen_ai.capture_message_content", - "default": false, - "description": "Determines whether Generative AI events include full content of user and assistant messages. Note that full content can have data privacy and size concerns and care should be taken when enabling this", - "name": "otel.instrumentation.genai.capture-message-content", - "type": "boolean", - "instrumentations": [ - "aws-sdk-2.2", - "openai-java-1.1" - ] - }, - { - "default": false, - "description": "Whether GraphQL operation name is added to the span name. WARNING: The GraphQL operation name is provided by the client and can have high cardinality. Use only when the server is not exposed to malicious clients.", - "name": "otel.instrumentation.graphql.add-operation-name-to-span-name.enabled", - "type": "boolean", - "instrumentations": [ - "graphql-java-12.0", - "graphql-java-20.0" - ] - }, - { - "declarative_name": "java.graphql.capture_query", - "default": true, - "description": "Whether to capture the query in `graphql.document` span attribute.", - "name": "otel.instrumentation.graphql.capture-query", - "type": "boolean", - "instrumentations": [ - "graphql-java-12.0", - "graphql-java-20.0" - ] - }, - { - "declarative_name": "java.graphql.data_fetcher.enabled", - "default": false, - "description": "Enables span generation for data fetchers.", - "name": "otel.instrumentation.graphql.data-fetcher.enabled", - "type": "boolean", - "instrumentations": [ - "graphql-java-20.0" - ] - }, - { - "declarative_name": "java.graphql.operation_name_in_span_name.enabled", - "default": false, - "description": "Whether GraphQL operation name is added to the span name. WARNING: The GraphQL operation name is provided by the client and can have high cardinality. Use only when the server is not exposed to malicious clients.", - "name": "otel.instrumentation.graphql.operation-name-in-span-name.enabled", - "type": "boolean", - "instrumentations": [ - "graphql-java-12.0", - "graphql-java-20.0" - ] - }, - { - "declarative_name": "java.graphql.query_sanitization.enabled", - "default": true, - "description": "Enables sanitization of sensitive information from queries so they aren't added as span attributes.", - "name": "otel.instrumentation.graphql.query-sanitization.enabled", - "type": "boolean", - "instrumentations": [ - "graphql-java-12.0", - "graphql-java-20.0" - ] - }, - { - "default": true, - "description": "Enables sanitization of sensitive information from queries so they aren't added as span attributes.", - "name": "otel.instrumentation.graphql.query-sanitizer.enabled", - "type": "boolean", - "instrumentations": [ - "graphql-java-12.0", - "graphql-java-20.0" - ] - }, - { - "declarative_name": "java.graphql.trivial_data_fetcher.enabled", - "default": false, - "description": "Whether to create spans for trivial data fetchers. A trivial data fetcher is one that simply maps data from an object to a field.", - "name": "otel.instrumentation.graphql.trivial-data-fetcher.enabled", - "type": "boolean", - "instrumentations": [ - "graphql-java-20.0" - ] - }, - { - "declarative_name": "java.grpc.capture_metadata.client.request", - "default": "", - "description": "A comma-separated list of request metadata keys. gRPC client instrumentation will capture metadata values corresponding to configured keys as span attributes.", - "examples": [ - "custom-request-header", - "my-metadata-key,another-metadata-key" - ], - "name": "otel.instrumentation.grpc.capture-metadata.client.request", - "type": "list", - "instrumentations": [ - "grpc-1.6" - ] - }, - { - "declarative_name": "java.grpc.capture_metadata.server.request", - "default": "", - "description": "A comma-separated list of request metadata keys. gRPC server instrumentation will capture metadata values corresponding to configured keys as span attributes.", - "name": "otel.instrumentation.grpc.capture-metadata.server.request", - "type": "list", - "instrumentations": [ - "grpc-1.6" - ] - }, - { - "declarative_name": "java.grpc.emit_message_events", - "default": true, - "description": "Determines whether to emit a span event for each individual message received and sent.", - "name": "otel.instrumentation.grpc.emit-message-events", - "type": "boolean", - "instrumentations": [ - "grpc-1.6" - ] - }, - { - "declarative_name": "java.grpc.experimental_span_attributes/development", - "default": false, - "description": "Enable the capture of experimental span attributes `grpc.received.message_count`, `grpc.sent.message_count` and `grpc.canceled`.", - "name": "otel.instrumentation.grpc.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "grpc-1.6" - ] - }, - { - "declarative_name": "java.guava.experimental_span_attributes/development", - "default": false, - "description": "Enables experimental span attribute `guava.canceled` for cancelled operations.", - "name": "otel.instrumentation.guava.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "guava-10.0" - ] - }, - { - "declarative_name": "java.hibernate.experimental_span_attributes/development", - "default": false, - "description": "Enables the experimental `hibernate.session_id` span attribute.", - "name": "otel.instrumentation.hibernate.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "hibernate-procedure-call-4.3", - "hibernate-6.0", - "hibernate-4.0", - "hibernate-3.3" - ] - }, - { - "declarative_name": "general.http.client.request_captured_headers", - "default": "", - "description": "List of HTTP request headers to capture in HTTP client telemetry.", - "name": "otel.instrumentation.http.client.capture-request-headers", - "type": "list", - "instrumentations": [ - "jetty-httpclient-12.0", - "async-http-client-1.9", - "jetty-httpclient-9.2", - "okhttp-2.2", - "reactor-netty-1.0", - "play-ws-1.0", - "play-ws-2.1", - "pekko-http-1.0", - "apache-httpclient-2.0", - "async-http-client-1.8", - "ktor-3.0", - "java-http-client", - "vertx-http-client-3.0", - "http-url-connection", - "apache-httpasyncclient-4.1", - "vertx-http-client-4.0", - "ratpack-1.7", - "netty-4.1", - "vertx-http-client-5.0", - "armeria-1.3", - "apache-httpclient-5.0", - "ktor-2.0", - "async-http-client-2.0", - "netty-3.8", - "akka-http-10.0", - "google-http-client-1.19", - "play-ws-2.0", - "netty-4.0", - "apache-httpclient-4.0", - "jodd-http-4.2", - "kubernetes-client-7.0", - "okhttp-3.0" - ] - }, - { - "declarative_name": "general.http.client.response_captured_headers", - "default": "", - "description": "List of HTTP response headers to capture in HTTP client telemetry.", - "name": "otel.instrumentation.http.client.capture-response-headers", - "type": "list", - "instrumentations": [ - "jetty-httpclient-12.0", - "async-http-client-1.9", - "jetty-httpclient-9.2", - "okhttp-2.2", - "reactor-netty-1.0", - "play-ws-1.0", - "play-ws-2.1", - "pekko-http-1.0", - "apache-httpclient-2.0", - "async-http-client-1.8", - "ktor-3.0", - "java-http-client", - "vertx-http-client-3.0", - "http-url-connection", - "apache-httpasyncclient-4.1", - "vertx-http-client-4.0", - "ratpack-1.7", - "netty-4.1", - "vertx-http-client-5.0", - "armeria-1.3", - "apache-httpclient-5.0", - "ktor-2.0", - "async-http-client-2.0", - "netty-3.8", - "akka-http-10.0", - "google-http-client-1.19", - "play-ws-2.0", - "netty-4.0", - "apache-httpclient-4.0", - "jodd-http-4.2", - "kubernetes-client-7.0", - "okhttp-3.0" - ] - }, - { - "declarative_name": "java.common.http.client.emit_experimental_telemetry/development", - "default": false, - "description": "Enable the capture of experimental HTTP client telemetry. Adds the `http.request.body.size` and `http.response.body.size` attributes to spans, and records `http.client.request.size` and `http.client.response.size` metrics.", - "name": "otel.instrumentation.http.client.emit-experimental-telemetry", - "type": "boolean", - "instrumentations": [ - "jetty-httpclient-12.0", - "async-http-client-1.9", - "jetty-httpclient-9.2", - "okhttp-2.2", - "reactor-netty-1.0", - "play-ws-1.0", - "play-ws-2.1", - "pekko-http-1.0", - "apache-httpclient-2.0", - "async-http-client-1.8", - "ktor-3.0", - "java-http-client", - "vertx-http-client-3.0", - "http-url-connection", - "apache-httpasyncclient-4.1", - "vertx-http-client-4.0", - "ratpack-1.7", - "netty-4.1", - "vertx-http-client-5.0", - "armeria-1.3", - "apache-httpclient-5.0", - "ktor-2.0", - "async-http-client-2.0", - "netty-3.8", - "akka-http-10.0", - "google-http-client-1.19", - "play-ws-2.0", - "netty-4.0", - "spring-web-6.0", - "apache-httpclient-4.0", - "jodd-http-4.2", - "kubernetes-client-7.0", - "okhttp-3.0" - ] - }, - { - "declarative_name": "java.common.http.client.redact_query_parameters/development", - "default": true, - "description": "Redact sensitive URL parameters. See https://opentelemetry.io/docs/specs/semconv/http/http-spans.", - "name": "otel.instrumentation.http.client.experimental.redact-query-parameters", - "type": "boolean", - "instrumentations": [ - "jetty-httpclient-12.0", - "async-http-client-1.9", - "jetty-httpclient-9.2", - "okhttp-2.2", - "reactor-netty-1.0", - "play-ws-1.0", - "play-ws-2.1", - "pekko-http-1.0", - "apache-httpclient-2.0", - "async-http-client-1.8", - "ktor-3.0", - "java-http-client", - "vertx-http-client-3.0", - "http-url-connection", - "apache-httpasyncclient-4.1", - "vertx-http-client-4.0", - "ratpack-1.7", - "netty-4.1", - "vertx-http-client-5.0", - "armeria-1.3", - "apache-httpclient-5.0", - "ktor-2.0", - "async-http-client-2.0", - "netty-3.8", - "akka-http-10.0", - "google-http-client-1.19", - "play-ws-2.0", - "netty-4.0", - "apache-httpclient-4.0", - "jodd-http-4.2", - "kubernetes-client-7.0", - "okhttp-3.0" - ] - }, - { - "declarative_name": "java.common.http.known_methods", - "default": "CONNECT,DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT,TRACE", - "description": "Configures the instrumentation to recognize an alternative set of HTTP request methods. All other methods will be treated as `_OTHER`.", - "name": "otel.instrumentation.http.known-methods", - "type": "list", - "instrumentations": [ - "jetty-httpclient-12.0", - "elasticsearch-rest-7.0", - "async-http-client-1.9", - "jetty-httpclient-9.2", - "okhttp-2.2", - "tomcat-10.0", - "reactor-netty-1.0", - "helidon-4.3", - "play-ws-1.0", - "play-ws-2.1", - "pekko-http-1.0", - "apache-httpclient-2.0", - "jetty-8.0", - "async-http-client-1.8", - "ktor-3.0", - "java-http-client", - "vertx-http-client-3.0", - "http-url-connection", - "aws-lambda-events-2.2", - "apache-httpasyncclient-4.1", - "vertx-http-client-4.0", - "ratpack-1.7", - "netty-4.1", - "restlet-1.1", - "vertx-http-client-5.0", - "armeria-1.3", - "liberty-20.0", - "apache-httpclient-5.0", - "ktor-2.0", - "elasticsearch-rest-6.4", - "async-http-client-2.0", - "netty-3.8", - "liberty-dispatcher-20.0", - "akka-http-10.0", - "google-http-client-1.19", - "play-ws-2.0", - "netty-4.0", - "elasticsearch-transport-5.0", - "grizzly-2.3", - "jetty-11.0", - "apache-httpclient-4.0", - "jodd-http-4.2", - "elasticsearch-rest-5.0", - "java-http-server", - "kubernetes-client-7.0", - "activej-http-6.0", - "restlet-2.0", - "okhttp-3.0", - "undertow-1.4", - "jetty-12.0", - "tomcat-7.0" - ] - }, - { - "default": "", - "description": "List of HTTP request headers to capture in HTTP server telemetry.", - "name": "otel.instrumentation.http.server.capture-request-headers", - "type": "list", - "instrumentations": [ - "tomcat-10.0", - "helidon-4.3", - "pekko-http-1.0", - "jetty-8.0", - "ktor-3.0", - "ratpack-1.7", - "netty-4.1", - "restlet-1.1", - "armeria-1.3", - "liberty-20.0", - "ktor-2.0", - "netty-3.8", - "liberty-dispatcher-20.0", - "akka-http-10.0", - "netty-4.0", - "grizzly-2.3", - "jetty-11.0", - "java-http-server", - "activej-http-6.0", - "restlet-2.0", - "undertow-1.4", - "jetty-12.0", - "tomcat-7.0" - ], - "declarative_name": "general.http.server.request_captured_headers" - }, - { - "default": "", - "description": "List of HTTP response headers to capture in HTTP server telemetry.", - "name": "otel.instrumentation.http.server.capture-response-headers", - "type": "list", - "instrumentations": [ - "tomcat-10.0", - "helidon-4.3", - "pekko-http-1.0", - "jetty-8.0", - "ktor-3.0", - "ratpack-1.7", - "netty-4.1", - "restlet-1.1", - "armeria-1.3", - "liberty-20.0", - "ktor-2.0", - "netty-3.8", - "liberty-dispatcher-20.0", - "akka-http-10.0", - "netty-4.0", - "grizzly-2.3", - "jetty-11.0", - "java-http-server", - "activej-http-6.0", - "restlet-2.0", - "undertow-1.4", - "jetty-12.0", - "tomcat-7.0" - ], - "declarative_name": "general.http.server.response_captured_headers" - }, - { - "default": false, - "description": "Enable the capture of experimental HTTP server telemetry. Adds the `http.request.body.size` and `http.response.body.size` attributes to spans, and records `http.server.request.body.size` and `http.server.response.body.size` metrics.", - "name": "otel.instrumentation.http.server.emit-experimental-telemetry", - "type": "boolean", - "instrumentations": [ - "tomcat-10.0", - "helidon-4.3", - "pekko-http-1.0", - "jetty-8.0", - "ktor-3.0", - "ratpack-1.7", - "netty-4.1", - "restlet-1.1", - "armeria-1.3", - "liberty-20.0", - "ktor-2.0", - "netty-3.8", - "liberty-dispatcher-20.0", - "akka-http-10.0", - "netty-4.0", - "grizzly-2.3", - "jetty-11.0", - "java-http-server", - "activej-http-6.0", - "restlet-2.0", - "undertow-1.4", - "jetty-12.0", - "tomcat-7.0" - ], - "declarative_name": "java.common.http.server.emit_experimental_telemetry/development" - }, - { - "default": false, - "description": "Enables capturing the experimental `hystrix.command`, `hystrix.circuit_open` and `hystrix.group` span attributes.", - "name": "otel.instrumentation.hystrix.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "hystrix-1.4" - ] - }, - { - "declarative_name": "java.java_util_logging.experimental_log_attributes/development", - "default": false, - "description": "Enables capturing the experimental `thread.name` and `thread.id` log attributes.", - "name": "otel.instrumentation.java-util-logging.experimental-log-attributes", - "type": "boolean", - "instrumentations": [ - "java-util-logging" - ] - }, - { - "declarative_name": "java.jaxrs.experimental_span_attributes/development", - "default": false, - "description": "Enables the experimental `jaxrs.canceled` span attribute.", - "name": "otel.instrumentation.jaxrs.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "jaxrs-3.0-resteasy-6.0", - "jaxrs-2.0-resteasy-3.0", - "jaxrs-2.0-jersey-2.0", - "jaxrs-3.0-jersey-3.0", - "jaxrs-2.0-cxf-3.2", - "jaxrs-2.0-annotations", - "jaxrs-3.0-annotations", - "jaxrs-2.0-resteasy-3.1" - ] - }, - { - "declarative_name": "java.jboss_logmanager.experimental_log_attributes/development", - "default": false, - "description": "Enables the capture of experimental log attributes, including thread name and thread ID.", - "name": "otel.instrumentation.jboss-logmanager.experimental-log-attributes", - "type": "boolean", - "instrumentations": [ - "jboss-logmanager-appender-1.1" - ] - }, - { - "declarative_name": "java.jboss_logmanager.capture_event_name/development", - "default": false, - "description": "Enables the capture of the event name from the MDC attribute \"event.name\" and sets it as the log record's event name.", - "name": "otel.instrumentation.jboss-logmanager.experimental.capture-event-name", - "type": "boolean", - "instrumentations": [ - "jboss-logmanager-appender-1.1" - ] - }, - { - "declarative_name": "java.jboss_logmanager.capture_mdc_attributes/development", - "default": "", - "description": "Controls which MDC attributes to capture. Use \"*\" to capture all MDC attributes or provide a comma-separated list of specific keys.", - "examples": [ - "custom-mdc-key", - "key1,key2,key3" - ], - "name": "otel.instrumentation.jboss-logmanager.experimental.capture-mdc-attributes", - "type": "list", - "instrumentations": [ - "jboss-logmanager-appender-1.1" - ] - }, - { - "default": false, - "description": "Enables instrumentation of JDBC datasource connections.", - "name": "otel.instrumentation.jdbc-datasource.enabled", - "type": "boolean", - "instrumentations": [ - "jdbc" - ] - }, - { - "default": false, - "description": "Sets whether the query parameters should be captured as span attributes named db.query.parameter.<key>. Enabling this option disables the statement sanitization.

WARNING: captured query parameters may contain sensitive information such as passwords, personally identifiable information or protected health info.", - "name": "otel.instrumentation.jdbc.experimental.capture-query-parameters", - "type": "boolean", - "instrumentations": [ - "jdbc" - ] - }, - { - "default": false, - "description": "Enables augmenting queries with a comment containing the tracing information. See [sqlcommenter](https://google.github.io/sqlcommenter/) for more info. WARNING: augmenting queries with tracing context will make query texts unique, which may have adverse impact on database performance. Consult with database experts before enabling.", - "name": "otel.instrumentation.jdbc.experimental.sqlcommenter.enabled", - "type": "boolean", - "instrumentations": [ - "jdbc" - ] - }, - { - "default": false, - "description": "Enables experimental instrumentation to create spans for COMMIT and ROLLBACK operations.", - "name": "otel.instrumentation.jdbc.experimental.transaction.enabled", - "type": "boolean", - "instrumentations": [ - "jdbc" - ] - }, - { - "default": true, - "description": "Enables query sanitization for database queries. Takes precedence over otel.instrumentation.common.db.query-sanitization.enabled.", - "name": "otel.instrumentation.jdbc.query-sanitization.enabled", - "type": "boolean", - "instrumentations": [ - "jdbc" - ] - }, - { - "default": true, - "description": "Enables statement sanitization for database queries. Takes precedent to otel.instrumentation.common.db-statement-sanitizer.enabled.", - "name": "otel.instrumentation.jdbc.statement-sanitizer.enabled", - "type": "boolean", - "instrumentations": [ - "jdbc" - ] - }, - { - "default": false, - "description": "Enables experimental span attributes `jsp.forwardOrigin`, `jsp.requestURL`, `jsp.compiler`, and `jsp.classFQCN`.", - "name": "otel.instrumentation.jsp.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "jsp-2.3" - ] - }, - { - "default": false, - "description": "Enables the capture of the experimental consumer attribute `kafka.record.queue_time_ms`.", - "name": "otel.instrumentation.kafka.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "kafka-streams-0.11", - "spring-kafka-2.7", - "reactor-kafka-1.0", - "kafka-clients-0.11", - "vertx-kafka-client-3.6" - ], - "declarative_name": "java.kafka.experimental_span_attributes/development" - }, - { - "declarative_name": "java.kafka.producer_propagation.enabled", - "default": true, - "description": "Enable context propagation for Kafka message producers.", - "name": "otel.instrumentation.kafka.producer-propagation.enabled", - "type": "boolean", - "instrumentations": [ - "kafka-clients-0.11", - "vertx-kafka-client-3.6" - ] - }, - { - "default": false, - "description": "Enables experimental span attributes `kubernetes-client.namespace` and `kubernetes-client.name` for Kubernetes API requests.", - "name": "otel.instrumentation.kubernetes-client.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "kubernetes-client-7.0" - ] - }, - { - "default": false, - "description": "Enables connection telemetry spans for Redis connections.", - "name": "otel.instrumentation.lettuce.connection-telemetry.enabled", - "type": "boolean", - "instrumentations": [ - "lettuce-5.0", - "lettuce-4.0" - ] - }, - { - "default": false, - "description": "Enables experimental span attributes `lettuce.command.cancelled` and `lettuce.command.results.count`.", - "name": "otel.instrumentation.lettuce.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "lettuce-5.0", - "lettuce-4.0" - ] - }, - { - "default": false, - "description": "Enables capturing `redis.encode.start` and `redis.encode.end` span events.", - "name": "otel.instrumentation.lettuce.experimental.command-encoding-events.enabled", - "type": "boolean", - "instrumentations": [ - "lettuce-5.1" - ] - }, - { - "default": false, - "description": "Enables the capture of experimental log attributes, including thread name and thread ID.", - "name": "otel.instrumentation.log4j-appender.experimental-log-attributes", - "type": "boolean", - "instrumentations": [ - "log4j-appender-1.2", - "log4j-appender-2.17" - ] - }, - { - "default": false, - "description": "Enables the capture of code location attributes, including file path, class name, method name, and line number.", - "name": "otel.instrumentation.log4j-appender.experimental.capture-code-attributes", - "type": "boolean", - "instrumentations": [ - "log4j-appender-1.2", - "log4j-appender-2.17" - ] - }, - { - "default": false, - "description": "Enables the capture of the event name from the MDC attribute \"event.name\" and sets it as the log record's event name.", - "name": "otel.instrumentation.log4j-appender.experimental.capture-event-name", - "type": "boolean", - "instrumentations": [ - "log4j-appender-1.2", - "log4j-appender-2.17" - ] - }, - { - "default": false, - "description": "Enables the capture of attributes from Log4j MapMessage instances.", - "name": "otel.instrumentation.log4j-appender.experimental.capture-map-message-attributes", - "type": "boolean", - "instrumentations": [ - "log4j-appender-2.17" - ] - }, - { - "default": false, - "description": "Enables the capture of the Log4j marker attribute.", - "name": "otel.instrumentation.log4j-appender.experimental.capture-marker-attribute", - "type": "boolean", - "instrumentations": [ - "log4j-appender-2.17" - ] - }, - { - "default": "", - "description": "Controls which MDC attributes to capture. Use \"*\" to capture all MDC attributes or provide a comma-separated list of specific keys.", - "name": "otel.instrumentation.log4j-appender.experimental.capture-mdc-attributes", - "type": "list", - "instrumentations": [ - "log4j-appender-1.2", - "log4j-appender-2.17" - ] - }, - { - "default": false, - "description": "Enables adding baggage entries to the Log4j ThreadContext, prefixed with \"baggage.\".", - "name": "otel.instrumentation.log4j-context-data.add-baggage", - "type": "boolean", - "instrumentations": [ - "log4j-context-data-2.7", - "log4j-context-data-2.17" - ] - }, - { - "declarative_name": "java.logback_appender.experimental_log_attributes/development", - "default": false, - "description": "Enables the capture of experimental log attributes, including thread name and thread ID.", - "name": "otel.instrumentation.logback-appender.experimental-log-attributes", - "type": "boolean", - "instrumentations": [ - "logback-appender-1.0" - ] - }, - { - "declarative_name": "java.logback_appender.capture_arguments/development", - "default": false, - "description": "Enables the capture of log message arguments as separate attributes.", - "name": "otel.instrumentation.logback-appender.experimental.capture-arguments", - "type": "boolean", - "instrumentations": [ - "logback-appender-1.0" - ] - }, - { - "declarative_name": "java.logback_appender.capture_code_attributes/development", - "default": false, - "description": "Enables the capture of code location attributes, including file path, class name, method name, and line number.", - "name": "otel.instrumentation.logback-appender.experimental.capture-code-attributes", - "type": "boolean", - "instrumentations": [ - "logback-appender-1.0" - ] - }, - { - "declarative_name": "java.logback_appender.capture_event_name/development", - "default": false, - "description": "Enables the capture of the event name from the MDC attribute \"event.name\" and sets it as the log record's event name.", - "name": "otel.instrumentation.logback-appender.experimental.capture-event-name", - "type": "boolean", - "instrumentations": [ - "logback-appender-1.0" - ] - }, - { - "declarative_name": "java.logback_appender.capture_key_value_pair_attributes/development", - "default": false, - "description": "Enables the capture of attributes from Logback key-value pairs.", - "name": "otel.instrumentation.logback-appender.experimental.capture-key-value-pair-attributes", - "type": "boolean", - "instrumentations": [ - "logback-appender-1.0" - ] - }, - { - "declarative_name": "java.logback_appender.capture_logger_context_attributes/development", - "default": false, - "description": "Enables the capture of attributes from the Logback logger context.", - "name": "otel.instrumentation.logback-appender.experimental.capture-logger-context-attributes", - "type": "boolean", - "instrumentations": [ - "logback-appender-1.0" - ] - }, - { - "declarative_name": "java.logback_appender.capture_logstash_marker_attributes/development", - "default": false, - "description": "Enables the capture of attributes from Logstash markers.", - "name": "otel.instrumentation.logback-appender.experimental.capture-logstash-marker-attributes", - "type": "boolean", - "instrumentations": [ - "logback-appender-1.0" - ] - }, - { - "declarative_name": "java.logback_appender.capture_logstash_structured_arguments/development", - "default": false, - "description": "Enables the capture of attributes from Logstash structured arguments.", - "name": "otel.instrumentation.logback-appender.experimental.capture-logstash-structured-arguments", - "type": "boolean", - "instrumentations": [ - "logback-appender-1.0" - ] - }, - { - "declarative_name": "java.logback_appender.capture_marker_attribute/development", - "default": false, - "description": "Enables the capture of the Logback marker attribute.", - "name": "otel.instrumentation.logback-appender.experimental.capture-marker-attribute", - "type": "boolean", - "instrumentations": [ - "logback-appender-1.0" - ] - }, - { - "declarative_name": "java.logback_appender.capture_mdc_attributes/development", - "default": "", - "description": "Controls which MDC attributes to capture. Use \"*\" to capture all MDC attributes or provide a comma-separated list of specific keys.", - "name": "otel.instrumentation.logback-appender.experimental.capture-mdc-attributes", - "type": "list", - "instrumentations": [ - "logback-appender-1.0" - ] - }, - { - "declarative_name": "java.logback_appender.capture_template/development", - "default": false, - "description": "Enables the capture of the log message template before parameter substitution.", - "name": "otel.instrumentation.logback-appender.experimental.capture-template", - "type": "boolean", - "instrumentations": [ - "logback-appender-1.0" - ] - }, - { - "default": false, - "description": "Enables adding baggage entries to the Logback MDC, prefixed with \"baggage.\".", - "name": "otel.instrumentation.logback-mdc.add-baggage", - "type": "boolean", - "instrumentations": [ - "logback-mdc-1.0" - ] - }, - { - "declarative_name": "java.common.messaging.capture_headers/development", - "default": "", - "description": "Allows configuring headers to capture as span attributes.", - "name": "otel.instrumentation.messaging.experimental.capture-headers", - "type": "list", - "instrumentations": [ - "aws-sdk-2.2", - "kafka-streams-0.11", - "jms-1.1", - "spring-kafka-2.7", - "pulsar-2.8", - "reactor-kafka-1.0", - "spring-rabbit-1.0", - "kafka-clients-0.11", - "jms-3.0", - "spring-integration-4.1", - "nats-2.17", - "rocketmq-client-5.0", - "aws-sdk-1.11", - "vertx-kafka-client-3.6", - "rocketmq-client-4.8", - "spring-jms-2.0", - "spring-pulsar-1.0", - "spring-jms-6.0" - ] - }, - { - "declarative_name": "java.common.messaging.receive_telemetry/development.enabled", - "default": false, - "description": "Enables experimental receive telemetry, which will cause consumers to start a new trace, with only a span link connecting it to the producer trace.", - "name": "otel.instrumentation.messaging.experimental.receive-telemetry.enabled", - "type": "boolean", - "instrumentations": [ - "aws-sdk-2.2", - "kafka-streams-0.11", - "jms-1.1", - "spring-kafka-2.7", - "pulsar-2.8", - "reactor-kafka-1.0", - "kafka-clients-0.11", - "jms-3.0", - "rocketmq-client-5.0", - "aws-sdk-1.11", - "vertx-kafka-client-3.6", - "spring-jms-2.0", - "spring-pulsar-1.0", - "spring-jms-6.0" - ] - }, - { - "default": "s", - "description": "Sets the base time unit for the OpenTelemetry MeterRegistry. Supported values: ns, us, ms, s, min, h, d.", - "name": "otel.instrumentation.micrometer.base-time-unit", - "type": "string", - "instrumentations": [ - "micrometer-1.5" - ] - }, - { - "default": false, - "description": "Enables gauge-based Micrometer histograms for DistributionSummary and Timer instruments.", - "name": "otel.instrumentation.micrometer.histogram-gauges.enabled", - "type": "boolean", - "instrumentations": [ - "micrometer-1.5" - ] - }, - { - "default": false, - "description": "Simulates the behavior of Micrometer's PrometheusMeterRegistry. The instruments will be renamed to match Micrometer instrument naming, and the base time unit will be set to seconds.", - "name": "otel.instrumentation.micrometer.prometheus-mode.enabled", - "type": "boolean", - "instrumentations": [ - "micrometer-1.5" - ] - }, - { - "default": true, - "description": "Enables query sanitization for MongoDB queries. Takes precedence over otel.instrumentation.common.db.query-sanitization.enabled.", - "name": "otel.instrumentation.mongo.query-sanitization.enabled", - "type": "boolean", - "instrumentations": [ - "mongo-async-3.3", - "mongo-3.7", - "mongo-4.0", - "mongo-3.1" - ] - }, - { - "default": true, - "description": "Enables statement sanitization for MongoDB queries. Takes precedence over otel.instrumentation.common.db-statement-sanitizer.enabled.", - "name": "otel.instrumentation.mongo.statement-sanitizer.enabled", - "type": "boolean", - "instrumentations": [ - "mongo-async-3.3", - "mongo-3.7", - "mongo-4.0", - "mongo-3.1" - ] - }, - { - "default": false, - "description": "Enable the creation of Connect and DNS spans.", - "name": "otel.instrumentation.netty.connection-telemetry.enabled", - "type": "boolean", - "instrumentations": [ - "netty-4.1", - "netty-4.0" - ] - }, - { - "default": false, - "description": "Enable SSL telemetry.", - "name": "otel.instrumentation.netty.ssl-telemetry.enabled", - "type": "boolean", - "instrumentations": [ - "netty-4.1", - "netty-4.0" - ] - }, - { - "default": false, - "description": "Enable the experimental `runtime.java.memory` and `runtime.java.cpu_time` metrics.", - "name": "otel.instrumentation.oshi.experimental-metrics.enabled", - "type": "boolean", - "instrumentations": [ - "oshi" - ] - }, - { - "default": false, - "description": "Enables experimental span attributes `job.system`, `scheduling.powerjob.job.id`, `scheduling.powerjob.job.param`, `scheduling.powerjob.job.instance.param`, and `scheduling.powerjob.job.type`.", - "name": "otel.instrumentation.powerjob.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "powerjob-4.0" - ] - }, - { - "default": false, - "description": "Enables the experimental span attribute `messaging.pulsar.message.type` for producer spans.", - "name": "otel.instrumentation.pulsar.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "pulsar-2.8", - "spring-pulsar-1.0" - ] - }, - { - "default": false, - "description": "Enables the experimental `job.system` span attribute.", - "name": "otel.instrumentation.quartz.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "quartz-2.0" - ] - }, - { - "default": false, - "description": "Enables augmenting queries with a comment containing the tracing information. See [sqlcommenter](https://google.github.io/sqlcommenter/) for more info. WARNING: augmenting queries with tracing context will make query texts unique, which may have adverse impact on database performance.", - "name": "otel.instrumentation.r2dbc.experimental.sqlcommenter.enabled", - "type": "boolean", - "instrumentations": [ - "r2dbc-1.0" - ] - }, - { - "default": true, - "description": "Enables query sanitization for database queries. Takes precedence over otel.instrumentation.common.db.query-sanitization.enabled.", - "name": "otel.instrumentation.r2dbc.query-sanitization.enabled", - "type": "boolean", - "instrumentations": [ - "r2dbc-1.0" - ] - }, - { - "default": true, - "description": "Enables statement sanitization for database queries. Takes precedence over otel.instrumentation.common.db-statement-sanitizer.enabled.", - "name": "otel.instrumentation.r2dbc.statement-sanitizer.enabled", - "type": "boolean", - "instrumentations": [ - "r2dbc-1.0" - ] - }, - { - "default": false, - "description": "Enables experimental span attributes `rabbitmq.command`, `rabbitmq.delivery_mode`, `rabbitmq.queue`, and `rabbitmq.record.queue_time_ms`.", - "name": "otel.instrumentation.rabbitmq.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "rabbitmq-2.7" - ] - }, - { - "default": false, - "description": "Enables the creation of Connect and DNS spans.", - "name": "otel.instrumentation.reactor-netty.connection-telemetry.enabled", - "type": "boolean", - "instrumentations": [ - "reactor-netty-1.0" - ] - }, - { - "default": false, - "description": "Enables the capture of the experimental `reactor.canceled` attribute on spans when reactive streams are cancelled.", - "name": "otel.instrumentation.reactor.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "reactor-3.1" - ] - }, - { - "default": false, - "description": "Enables capturing experimental span attributes `messaging.rocketmq.message.tag`, `messaging.rocketmq.broker_address`, `messaging.rocketmq.send_result`, `messaging.rocketmq.queue_id`, and `messaging.rocketmq.queue_offset`.", - "name": "otel.instrumentation.rocketmq-client.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "rocketmq-client-4.8" - ] - }, - { - "default": false, - "description": "Enables the capture of experimental JVM runtime metrics.", - "name": "otel.instrumentation.runtime-telemetry.emit-experimental-metrics", - "type": "boolean", - "instrumentations": [ - "runtime-telemetry" - ] - }, - { - "default": false, - "description": "Enables creating events for JAR libraries used by the application.", - "name": "otel.instrumentation.runtime-telemetry.experimental.package-emitter.enabled", - "type": "boolean", - "instrumentations": [ - "runtime-telemetry" - ] - }, - { - "default": 10, - "description": "The number of JAR files processed per second by the package emitter.", - "name": "otel.instrumentation.runtime-telemetry.experimental.package-emitter.jars-per-second", - "type": "int", - "instrumentations": [ - "runtime-telemetry" - ] - }, - { - "default": false, - "description": "Prefers JFR over JMX for metrics where both collection methods are available (Java 17+).", - "name": "otel.instrumentation.runtime-telemetry.experimental.prefer-jfr", - "type": "boolean", - "instrumentations": [ - "runtime-telemetry" - ] - }, - { - "default": false, - "description": "Enables the experimental span attribute `rxjava.canceled`.", - "name": "otel.instrumentation.rxjava.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "rxjava-3.0", - "rxjava-3.1.1", - "rxjava-2.0" - ] - }, - { - "default": true, - "description": "Adds the trace ID and span ID as a request attributes for downstream servlet access.", - "name": "otel.instrumentation.servlet.add-trace-id-request-attribute", - "type": "boolean", - "instrumentations": [ - "servlet-3.0", - "servlet-5.0", - "servlet-2.2" - ] - }, - { - "default": "", - "description": "List of request parameter names to capture as span attributes.", - "name": "otel.instrumentation.servlet.capture-request-parameters", - "type": "list", - "instrumentations": [ - "tomcat-10.0", - "servlet-3.0", - "servlet-5.0", - "servlet-2.2" - ] - }, - { - "default": false, - "description": "Enables capturing the experimental `servlet.timeout` span attribute.", - "name": "otel.instrumentation.servlet.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "tomcat-10.0", - "servlet-3.0", - "servlet-5.0", - "servlet-2.2", - "liberty-20.0", - "tomcat-7.0" - ], - "declarative_name": "java.servlet.experimental_span_attributes/development" - }, - { - "default": "", - "description": "List of request parameter names to capture as span attributes.", - "name": "otel.instrumentation.servlet.experimental.capture-request-parameters", - "type": "list", - "instrumentations": [ - "servlet-3.0", - "servlet-5.0", - "servlet-2.2", - "liberty-20.0", - "tomcat-7.0" - ], - "declarative_name": "java.servlet.capture_request_parameters/development" - }, - { - "default": true, - "description": "Enables adding the trace ID and span ID as request attributes for downstream servlet access.", - "name": "otel.instrumentation.servlet.experimental.trace-id-request-attribute.enabled", - "type": "boolean", - "instrumentations": [ - "servlet-3.0", - "servlet-5.0", - "servlet-2.2" - ] - }, - { - "default": false, - "description": "Adds the experimental attribute `job.system` to spans.", - "name": "otel.instrumentation.spring-batch.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "spring-batch-3.0" - ] - }, - { - "default": false, - "description": "When enabled, a new root span will be created for each chunk processing. Please note that this may lead to a high number of spans being created.", - "name": "otel.instrumentation.spring-batch.experimental.chunk.new-trace", - "type": "boolean", - "instrumentations": [ - "spring-batch-3.0" - ] - }, - { - "default": false, - "description": "When enabled, spans will be created for each item processed. Please note that this may lead to a high number of spans being created.", - "name": "otel.instrumentation.spring-batch.item.enabled", - "type": "boolean", - "instrumentations": [ - "spring-batch-3.0" - ] - }, - { - "default": false, - "description": "Enables experimental `spring-cloud-gateway.route` attributes (e.g., `spring-cloud-gateway.route.id`, `spring-cloud-gateway.route.uri`, etc.) on spans.", - "name": "otel.instrumentation.spring-cloud-gateway.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "spring-cloud-gateway-2.0", - "spring-cloud-gateway-webmvc-4.3" - ] - }, - { - "default": "*", - "description": "A list of Spring channel name patterns that will be intercepted.", - "name": "otel.instrumentation.spring-integration.global-channel-interceptor-patterns", - "type": "list", - "instrumentations": [ - "spring-integration-4.1" - ] - }, - { - "default": false, - "description": "Create producer spans when messages are sent to an output channel. Enable when you're using a messaging library that doesn't have its own instrumentation for generating producer spans. Note that the detection of output channels only works for Spring Cloud Stream `DirectWithAttributesChannel`.", - "name": "otel.instrumentation.spring-integration.producer.enabled", - "type": "boolean", - "instrumentations": [ - "spring-integration-4.1" - ] - }, - { - "default": false, - "description": "Adds the experimental span attribute `job.system` with the value `spring_scheduling`.", - "name": "otel.instrumentation.spring-scheduling.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "spring-scheduling-3.1" - ] - }, - { - "default": "ROLE_", - "description": "Prefix of granted authorities identifying roles to capture in the `enduser.role` semantic attribute.", - "name": "otel.instrumentation.spring-security.enduser.role.granted-authority-prefix", - "type": "string", - "instrumentations": [ - "spring-security-config-6.0" - ] - }, - { - "default": "SCOPE_", - "description": "Prefix of granted authorities identifying scopes to capture in the `enduser.scopes` semantic attribute.", - "name": "otel.instrumentation.spring-security.scope.role.granted-authority-prefix", - "type": "string", - "instrumentations": [ - "spring-security-config-6.0" - ] - }, - { - "default": false, - "description": "Enables the capture of experimental span attributes `spring-webmvc.view.name` and `spring-webmvc.view.type`.", - "name": "otel.instrumentation.spring-webmvc.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "spring-webmvc-6.0", - "spring-webmvc-3.1" - ] - }, - { - "default": false, - "description": "Enables experimental span attributes `spymemcached.result` and `spymemcached.command.cancelled`.", - "name": "otel.instrumentation.spymemcached.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "spymemcached-2.12" - ] - }, - { - "default": false, - "description": "Enables experimental span attributes `twilio.type`, `twilio.account`, `twilio.sid`, and `twilio.status`.", - "name": "otel.instrumentation.twilio.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "twilio-6.6" - ] - }, - { - "default": false, - "description": "Enables experimental span attributes `job.system`, `scheduling.xxl-job.glue.type`, and `scheduling.xxl-job.job.id`.", - "name": "otel.instrumentation.xxl-job.experimental-span-attributes", - "type": "boolean", - "instrumentations": [ - "xxl-job-2.1.2", - "xxl-job-1.9.2", - "xxl-job-2.3.0" - ] - }, - { - "default": "", - "description": "Opt-in to emit stable semantic conventions instead of the old experimental semantic conventions. Accepts a comma-separated list of semantic convention groups (e.g., `database`, `http`, `messaging`). Use `/dup` to emit both old and new conventions simultaneously. Stable semantic conventions will become the default in version 3.0 of the agent.", - "name": "otel.semconv-stability.opt-in", - "type": "list", - "instrumentations": [ - "apache-dbcp-2.0", - "alibaba-druid-1.0", - "tomcat-jdbc" - ] - } -] \ No newline at end of file diff --git a/ecosystem-explorer/src/App.test.tsx b/ecosystem-explorer/src/App.test.tsx index dfa1a310..7903eb47 100644 --- a/ecosystem-explorer/src/App.test.tsx +++ b/ecosystem-explorer/src/App.test.tsx @@ -23,19 +23,18 @@ describe("App", () => { vi.unstubAllEnvs(); }); - it("renders the legacy app when V1_REDESIGN is disabled", () => { + it("renders the legacy app when V1_REDESIGN is disabled", async () => { vi.stubEnv("VITE_FEATURE_FLAG_V1_REDESIGN", ""); - const { container } = render( + render( ); - // V1App adds .v1-app to scope its token overrides; absence confirms legacy rendering. - expect(container.querySelector(".v1-app")).toBeNull(); - // LegacyApp's Header renders "OTel Explorer" synchronously — no lazy load needed. - expect(screen.getByText("OTel Explorer")).toBeInTheDocument(); + const heading = await screen.findByRole("heading", { level: 1 }); + expect(heading).toHaveTextContent("OpenTelemetry"); + expect(heading).toHaveTextContent("Ecosystem Explorer"); }); it("renders the v1 app when V1_REDESIGN is enabled", async () => { diff --git a/ecosystem-explorer/src/LegacyApp.tsx b/ecosystem-explorer/src/LegacyApp.tsx index 8d77c183..bce83f7e 100644 --- a/ecosystem-explorer/src/LegacyApp.tsx +++ b/ecosystem-explorer/src/LegacyApp.tsx @@ -109,10 +109,12 @@ export function LegacyApp() { {isEnabled("JAVA_RELEASE_COMPARISON") && ( } /> )} - } - /> + {isEnabled("JAVA_CONFIG_BUILDER") && ( + } + /> + )} } /> {isEnabled("COLLECTOR_PAGE") && ( <> diff --git a/ecosystem-explorer/src/components/ui/beta-badge.tsx b/ecosystem-explorer/src/components/ui/beta-badge.tsx deleted file mode 100644 index 9fd4dbd6..00000000 --- a/ecosystem-explorer/src/components/ui/beta-badge.tsx +++ /dev/null @@ -1,24 +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 type { JSX } from "react"; - -export function BetaBadge(): JSX.Element { - return ( - - Beta - - ); -} diff --git a/ecosystem-explorer/src/components/ui/detail-card.tsx b/ecosystem-explorer/src/components/ui/detail-card.tsx index 0bb57fe5..4602f626 100644 --- a/ecosystem-explorer/src/components/ui/detail-card.tsx +++ b/ecosystem-explorer/src/components/ui/detail-card.tsx @@ -14,15 +14,12 @@ * limitations under the License. */ import React from "react"; -import type { CollectorComponentType } from "./type-stripe-colors"; -import { TypeStripe } from "./type-stripe"; interface DetailCardProps { children: React.ReactNode; className?: string; withGrid?: boolean; withHoverEffect?: boolean; - typeStripe?: CollectorComponentType; } export function DetailCard({ @@ -30,7 +27,6 @@ export function DetailCard({ className = "", withGrid = false, withHoverEffect = false, - typeStripe, }: DetailCardProps) { const patternId = React.useId().replace(/:/g, "-"); @@ -42,12 +38,6 @@ export function DetailCard({ : "" } ${className}`} > - {typeStripe && ( - - )} {withGrid && (

diff --git a/ecosystem-explorer/src/components/ui/type-stripe-colors.ts b/ecosystem-explorer/src/components/ui/type-stripe-colors.ts deleted file mode 100644 index 48486e4c..00000000 --- a/ecosystem-explorer/src/components/ui/type-stripe-colors.ts +++ /dev/null @@ -1,34 +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. - */ - -/* - * TypeStripe color map — shared between the standalone `` - * primitive and the `typeStripe` slot on ``. Lives in its own - * file so React Fast Refresh stays happy (`react-refresh/only-export-components` - * disallows constants alongside component exports). - */ - -import type { CollectorComponent } from "@/types/collector"; - -export type CollectorComponentType = CollectorComponent["type"]; - -export const TYPE_STRIPE_COLORS: Record = { - receiver: "hsl(200 85% 45%)", - processor: "hsl(270 70% 55%)", - exporter: "hsl(38 95% 52%)", - connector: "hsl(330 75% 50%)", - extension: "hsl(165 70% 40%)", -}; diff --git a/ecosystem-explorer/src/components/ui/type-stripe.test.tsx b/ecosystem-explorer/src/components/ui/type-stripe.test.tsx deleted file mode 100644 index f640d59a..00000000 --- a/ecosystem-explorer/src/components/ui/type-stripe.test.tsx +++ /dev/null @@ -1,45 +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 } from "@testing-library/react"; -import { describe, it, expect } from "vitest"; -import { type CollectorComponentType, TYPE_STRIPE_COLORS } from "./type-stripe-colors"; -import { TypeStripe } from "./type-stripe"; - -const types: CollectorComponentType[] = [ - "receiver", - "processor", - "exporter", - "connector", - "extension", -]; - -describe("TypeStripe", () => { - it.each(types)("renders an aria-hidden element with type=%s and the matching color", (type) => { - const { container } = render(); - const el = container.querySelector(".type-stripe"); - expect(el).not.toBeNull(); - expect(el).toHaveAttribute("data-type", type); - expect(el).toHaveAttribute("aria-hidden"); - expect(el).toHaveStyle({ backgroundColor: TYPE_STRIPE_COLORS[type] }); - }); - - it("exports a stable color for each of the five canonical types", () => { - expect(Object.keys(TYPE_STRIPE_COLORS).sort()).toEqual(types.slice().sort()); - for (const c of Object.values(TYPE_STRIPE_COLORS)) { - expect(c).toMatch(/^hsl\(/); - } - }); -}); diff --git a/ecosystem-explorer/src/components/ui/type-stripe.tsx b/ecosystem-explorer/src/components/ui/type-stripe.tsx deleted file mode 100644 index 6a674066..00000000 --- a/ecosystem-explorer/src/components/ui/type-stripe.tsx +++ /dev/null @@ -1,49 +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. - */ - -/* - * TypeStripe — 4px left-edge color stripe used to flag the OTel-collector - * component type at the leading edge of cards, list rows, and detail headers. - * - * Five canonical types: receiver / processor / exporter / connector / - * extension. Color mapping lives in `./type-stripe-colors.ts` (split out so - * React Fast Refresh stays happy). - * - * Two consumers: - * 1. As a slot inside `` — DetailCard mounts - * `` with its own positioning className, so the stripe - * rendering stays in one place. - * 2. As a standalone `` for list rows that don't - * use DetailCard (e.g. compact list view in Phase 4). - */ - -import { type CollectorComponentType, TYPE_STRIPE_COLORS } from "./type-stripe-colors"; - -export interface TypeStripeProps { - type: CollectorComponentType; - className?: string; -} - -export function TypeStripe({ type, className }: TypeStripeProps) { - return ( - - ); -} diff --git a/ecosystem-explorer/src/features/java-agent/components/agent-explore-landing.test.tsx b/ecosystem-explorer/src/features/java-agent/components/agent-explore-landing.test.tsx deleted file mode 100644 index 15550cc7..00000000 --- a/ecosystem-explorer/src/features/java-agent/components/agent-explore-landing.test.tsx +++ /dev/null @@ -1,31 +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 } from "vitest"; -import { render, screen } from "@testing-library/react"; -import { BrowserRouter } from "react-router-dom"; -import { AgentExploreLanding } from "./agent-explore-landing"; - -describe("AgentExploreLanding", () => { - it("renders the Configuration Builder card linking to the builder route", () => { - render( - - - - ); - const link = screen.getByRole("link", { name: /configuration builder/i }); - expect(link).toHaveAttribute("href", "/java-agent/configuration/builder"); - }); -}); diff --git a/ecosystem-explorer/src/features/java-agent/components/agent-explore-landing.tsx b/ecosystem-explorer/src/features/java-agent/components/agent-explore-landing.tsx index 7b3cee5f..b261f957 100644 --- a/ecosystem-explorer/src/features/java-agent/components/agent-explore-landing.tsx +++ b/ecosystem-explorer/src/features/java-agent/components/agent-explore-landing.tsx @@ -36,12 +36,6 @@ export function AgentExploreLanding() { href="/java-agent/configuration" icon={} /> - } - /> {isEnabled("JAVA_RELEASE_COMPARISON") && ( { expect(standaloneButton.className).toContain(FILTER_STYLES.target.library.inactive); }); - it("shows 'Clear all' button when any filter is active", () => { - const activeFilters: FilterState = { - search: "http", - telemetry: new Set(["spans"]), - target: new Set(), - semantic: [], - features: [], - }; - render( - - ); - - expect(screen.getByRole("button", { name: "Clear all" })).toBeInTheDocument(); - }); - - it("does not show 'Clear all' button when no filters are active", () => { - render( - - ); - - expect(screen.queryByRole("button", { name: "Clear all" })).not.toBeInTheDocument(); - }); - - it("clears all filters when 'Clear all' is clicked", async () => { - const user = userEvent.setup(); - const onFiltersChange = vi.fn(); - const activeFilters: FilterState = { - search: "http", - telemetry: new Set(["spans"]), - target: new Set(["javaagent"]), - semantic: ["db"], - features: ["db.client.connections"], - }; - render( - - ); - - await user.click(screen.getByRole("button", { name: "Clear all" })); - - expect(onFiltersChange).toHaveBeenCalledWith({ - search: "", - telemetry: new Set(), - target: new Set(), - semantic: [], - features: [], - }); - }); - it("sets aria-pressed attribute correctly for toggle buttons", () => { const activeFilters: FilterState = { search: "", diff --git a/ecosystem-explorer/src/features/java-agent/components/instrumentation-filter-bar.tsx b/ecosystem-explorer/src/features/java-agent/components/instrumentation-filter-bar.tsx index 494535b8..119c2c71 100644 --- a/ecosystem-explorer/src/features/java-agent/components/instrumentation-filter-bar.tsx +++ b/ecosystem-explorer/src/features/java-agent/components/instrumentation-filter-bar.tsx @@ -102,28 +102,6 @@ export function InstrumentationFilterBar({
- {(filters.search !== "" || - filters.telemetry.size > 0 || - filters.target.size > 0 || - filters.semantic.length > 0 || - filters.features.length > 0) && ( -
- -
- )}
); @@ -135,16 +125,14 @@ function SdkTabContent({ interface InstrumentationTabContentProps { schema: GroupNode; starter: ReturnType["data"]; - schemaVersion: string; - javaAgentVersion: string; + version: string; activeTab: string; } function InstrumentationTabContent({ schema, starter, - schemaVersion, - javaAgentVersion, + version, activeTab, }: InstrumentationTabContentProps) { const generalNode = useMemo(() => { @@ -156,18 +144,8 @@ function InstrumentationTabContent({ }, [schema]); return ( - - + + ); } @@ -176,18 +154,25 @@ interface InstrumentationTabBodyProps { activeTab: string; schema: GroupNode; generalNode: GroupNode | null; - javaAgentVersion: string; } -function InstrumentationTabBody({ - activeTab, - schema, - generalNode, - javaAgentVersion, -}: InstrumentationTabBodyProps) { +function InstrumentationTabBody({ activeTab, schema, generalNode }: InstrumentationTabBodyProps) { const [search, setSearch] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); + // The page-level `version` is the SDK config schema version (e.g. "1.0.0"). + // The instrumentation registry is keyed by Java agent version (e.g. "2.27.0") + // — a separate namespace. Resolve the latest agent version for the browser + // until a unified version picker lands as a follow-up. + const javaAgentVersions = useJavaAgentVersions(); + const javaAgentVersion = useMemo( + () => + javaAgentVersions.data?.versions.find((v) => v.is_latest)?.version ?? + javaAgentVersions.data?.versions[0]?.version ?? + "", + [javaAgentVersions.data] + ); + const tocSections: TocSection[] = useMemo( () => [ { key: GENERAL_SECTION_KEY, label: GENERAL_SETTINGS_LABEL }, @@ -261,39 +246,26 @@ function InstrumentationTabBody({ onJumpToGeneral={scrollToSection} />
- +
); } export function ConfigurationBuilderPage() { - const schemaVersionsState = useConfigVersions(); - const latestSchemaVersion = useMemo( + const versions = useConfigVersions(); + const latest = useMemo( () => - schemaVersionsState.data?.versions.find((v) => v.is_latest)?.version ?? - schemaVersionsState.data?.versions[0]?.version ?? + versions.data?.versions.find((v) => v.is_latest)?.version ?? + versions.data?.versions[0]?.version ?? "", - [schemaVersionsState.data] + [versions.data] ); - const [currentSchemaVersion, setCurrentSchemaVersion] = useState(""); - const schemaVersion = currentSchemaVersion || latestSchemaVersion; + const [currentVersion, setCurrentVersion] = useState(""); + const version = currentVersion || latest; const [activeTab, setActiveTab] = useState("sdk"); - const javaAgentVersionsState = useVersions(); - const javaAgentVersions = useMemo( - () => javaAgentVersionsState.data?.versions ?? [], - [javaAgentVersionsState.data] - ); - const latestJavaAgentVersion = useMemo( - () => - javaAgentVersions.find((v) => v.is_latest)?.version ?? javaAgentVersions[0]?.version ?? "", - [javaAgentVersions] - ); - const [currentJavaAgentVersion, setCurrentJavaAgentVersion] = useState(""); - const javaAgentVersion = currentJavaAgentVersion || latestJavaAgentVersion; - - const schema = useConfigSchema(schemaVersion); - const starter = useConfigStarter(schemaVersion); + const schema = useConfigSchema(version); + const starter = useConfigStarter(version); const root = (schema.data as GroupNode | null) ?? null; return ( @@ -302,87 +274,52 @@ export function ConfigurationBuilderPage() {
-
-

- - Configuration Builder - -

- -
+

+ + Configuration Builder + +

- Build and customize your OpenTelemetry Java Agent configuration.{" "} - - Learn more about declarative configuration - {" "} - ·{" "} - - Report an issue - + Build and customize your OpenTelemetry Java Agent configuration

-
- {schemaVersionsState.data && schemaVersion ? ( - - ) : null} - {javaAgentVersions.length > 0 && javaAgentVersion ? ( - - ) : null} -
+ {versions.data && version ? ( + + ) : null}
- {!schemaVersion || schema.loading || starter.loading ? ( + {schema.loading || starter.loading ? (

Loading schema…

- ) : schema.error ? ( + ) : schema.error || !root ? (

Failed to load schema.

) : starter.error ? (

Failed to load starter template.

- ) : root ? ( + ) : version ? ( ) : null}
- {!schemaVersion || schema.loading || starter.loading ? ( + {schema.loading || starter.loading ? (

Loading schema…

- ) : schema.error ? ( + ) : schema.error || !root ? (

Failed to load schema.

) : starter.error ? (

Failed to load starter template.

- ) : 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: