Skip to content

Commit 94784d9

Browse files
authored
feat(explorer): display semantic conventions as linked badges on inst… (#220)
1 parent 77ae9bd commit 94784d9

4 files changed

Lines changed: 197 additions & 7 deletions

File tree

ecosystem-explorer/src/features/java-agent/instrumentation-detail-page.test.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ const mockInstrumentation: InstrumentationData = {
5656
has_standalone_library: true,
5757
};
5858

59+
const mockInstrumentationWithSemconv: InstrumentationData = {
60+
...mockInstrumentation,
61+
semantic_conventions: ["HTTP_CLIENT_SPANS", "DATABASE_CLIENT_SPANS", "UNKNOWN_CONVENTION"],
62+
};
63+
5964
function renderWithRouter(initialPath: string) {
6065
return render(
6166
<MemoryRouter initialEntries={[initialPath]}>
@@ -171,4 +176,55 @@ describe("InstrumentationDetailPage", () => {
171176
replace: true,
172177
});
173178
});
179+
180+
it("renders semantic conventions as linked badges for known values", () => {
181+
vi.mocked(useInstrumentation).mockReturnValue({
182+
data: mockInstrumentationWithSemconv,
183+
loading: false,
184+
error: null,
185+
});
186+
187+
renderWithRouter("/java-agent/instrumentation/2.0.0/jdbc");
188+
189+
expect(screen.getByRole("heading", { name: "Semantic Conventions" })).toBeInTheDocument();
190+
191+
const httpLink = screen.getByRole("link", { name: "HTTP Client Spans" });
192+
expect(httpLink).toBeInTheDocument();
193+
expect(httpLink).toHaveAttribute(
194+
"href",
195+
"https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-client-span"
196+
);
197+
expect(httpLink).toHaveAttribute("target", "_blank");
198+
199+
const dbLink = screen.getByRole("link", { name: "Database Client Spans" });
200+
expect(dbLink).toBeInTheDocument();
201+
expect(dbLink).toHaveAttribute(
202+
"href",
203+
"https://opentelemetry.io/docs/specs/semconv/database/database-spans/"
204+
);
205+
});
206+
207+
it("renders unknown semantic conventions as plain text", () => {
208+
vi.mocked(useInstrumentation).mockReturnValue({
209+
data: mockInstrumentationWithSemconv,
210+
loading: false,
211+
error: null,
212+
});
213+
214+
renderWithRouter("/java-agent/instrumentation/2.0.0/jdbc");
215+
216+
expect(screen.getByText("UNKNOWN_CONVENTION")).toBeInTheDocument();
217+
});
218+
219+
it("does not render semantic conventions section when none are present", () => {
220+
vi.mocked(useInstrumentation).mockReturnValue({
221+
data: mockInstrumentation,
222+
loading: false,
223+
error: null,
224+
});
225+
226+
renderWithRouter("/java-agent/instrumentation/2.0.0/jdbc");
227+
228+
expect(screen.queryByRole("heading", { name: "Semantic Conventions" })).not.toBeInTheDocument();
229+
});
174230
});

ecosystem-explorer/src/features/java-agent/instrumentation-detail-page.tsx

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import { GlowBadge } from "@/components/ui/glow-badge";
3131
import { DetailCard } from "@/components/ui/detail-card";
3232
import { SectionHeader } from "@/components/ui/section-header";
3333
import { useVersions, useInstrumentation } from "@/hooks/use-javaagent-data";
34-
import { getInstrumentationDisplayName } from "./utils/format";
34+
import { getInstrumentationDisplayName, getSemanticConventionInfo } from "./utils/format";
3535
import { TelemetrySection } from "./components/telemetry-section";
3636

3737
function buildSourceUrl(sourcePath: string): string {
@@ -394,11 +394,27 @@ export function InstrumentationDetailPage() {
394394
Semantic Conventions
395395
</h3>
396396
<div className="flex flex-wrap gap-2">
397-
{instrumentation.semantic_conventions.map((convention) => (
398-
<GlowBadge key={convention} variant="muted">
399-
{convention}
400-
</GlowBadge>
401-
))}
397+
{instrumentation.semantic_conventions.map((convention) => {
398+
const info = getSemanticConventionInfo(convention);
399+
if (info) {
400+
return (
401+
<a
402+
key={convention}
403+
href={info.url}
404+
target="_blank"
405+
rel="noopener noreferrer"
406+
className="px-3 py-1 text-sm rounded-md border border-primary/40 bg-primary/10 text-primary hover:bg-primary/20 transition-colors"
407+
>
408+
{info.label}
409+
</a>
410+
);
411+
}
412+
return (
413+
<GlowBadge key={convention} variant="muted">
414+
{convention}
415+
</GlowBadge>
416+
);
417+
})}
402418
</div>
403419
</div>
404420
</DetailCard>

ecosystem-explorer/src/features/java-agent/utils/format.test.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616
import { describe, it, expect } from "vitest";
17-
import { getInstrumentationDisplayName } from "./format";
17+
import { getInstrumentationDisplayName, getSemanticConventionInfo } from "./format";
1818
import type { InstrumentationData } from "@/types/javaagent";
1919

2020
describe("getInstrumentationDisplayName", () => {
@@ -82,3 +82,45 @@ describe("getInstrumentationDisplayName", () => {
8282
expect(getInstrumentationDisplayName(instrumentation)).toBe("Jdbc");
8383
});
8484
});
85+
86+
describe("getSemanticConventionInfo", () => {
87+
it("returns label and url for a known value", () => {
88+
const info = getSemanticConventionInfo("HTTP_CLIENT_SPANS");
89+
expect(info).toEqual({
90+
label: "HTTP Client Spans",
91+
url: "https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-client-span",
92+
});
93+
});
94+
95+
it("returns label and url for DATABASE_CLIENT_SPANS", () => {
96+
const info = getSemanticConventionInfo("DATABASE_CLIENT_SPANS");
97+
expect(info).toEqual({
98+
label: "Database Client Spans",
99+
url: "https://opentelemetry.io/docs/specs/semconv/database/database-spans/",
100+
});
101+
});
102+
103+
it("returns label and url for MESSAGING_SPANS", () => {
104+
const info = getSemanticConventionInfo("MESSAGING_SPANS");
105+
expect(info).toEqual({
106+
label: "Messaging Spans",
107+
url: "https://opentelemetry.io/docs/specs/semconv/messaging/messaging-spans/",
108+
});
109+
});
110+
111+
it("returns label and url for GENAI_CLIENT_SPANS", () => {
112+
const info = getSemanticConventionInfo("GENAI_CLIENT_SPANS");
113+
expect(info).toEqual({
114+
label: "GenAI Client Spans",
115+
url: "https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/",
116+
});
117+
});
118+
119+
it("returns null for an unknown value", () => {
120+
expect(getSemanticConventionInfo("UNKNOWN_CONVENTION")).toBeNull();
121+
});
122+
123+
it("returns null for an empty string", () => {
124+
expect(getSemanticConventionInfo("")).toBeNull();
125+
});
126+
});

ecosystem-explorer/src/features/java-agent/utils/format.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,82 @@
1515
*/
1616
import type { InstrumentationData } from "@/types/javaagent";
1717

18+
export interface SemanticConventionInfo {
19+
label: string;
20+
url: string;
21+
}
22+
23+
const SEMANTIC_CONVENTION_MAP: Record<string, SemanticConventionInfo> = {
24+
HTTP_CLIENT_SPANS: {
25+
label: "HTTP Client Spans",
26+
url: "https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-client-span",
27+
},
28+
HTTP_SERVER_SPANS: {
29+
label: "HTTP Server Spans",
30+
url: "https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-server-span",
31+
},
32+
HTTP_CLIENT_METRICS: {
33+
label: "HTTP Client Metrics",
34+
url: "https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#http-client",
35+
},
36+
HTTP_SERVER_METRICS: {
37+
label: "HTTP Server Metrics",
38+
url: "https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#http-server",
39+
},
40+
DATABASE_CLIENT_SPANS: {
41+
label: "Database Client Spans",
42+
url: "https://opentelemetry.io/docs/specs/semconv/database/database-spans/",
43+
},
44+
DATABASE_CLIENT_METRICS: {
45+
label: "Database Client Metrics",
46+
url: "https://opentelemetry.io/docs/specs/semconv/database/database-metrics/",
47+
},
48+
DATABASE_POOL_METRICS: {
49+
label: "Database Pool Metrics",
50+
url: "https://opentelemetry.io/docs/specs/semconv/database/database-metrics/#connection-pools",
51+
},
52+
MESSAGING_SPANS: {
53+
label: "Messaging Spans",
54+
url: "https://opentelemetry.io/docs/specs/semconv/messaging/messaging-spans/",
55+
},
56+
RPC_CLIENT_SPANS: {
57+
label: "RPC Client Spans",
58+
url: "https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/",
59+
},
60+
RPC_SERVER_SPANS: {
61+
label: "RPC Server Spans",
62+
url: "https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/",
63+
},
64+
RPC_CLIENT_METRICS: {
65+
label: "RPC Client Metrics",
66+
url: "https://opentelemetry.io/docs/specs/semconv/rpc/rpc-metrics/",
67+
},
68+
RPC_SERVER_METRICS: {
69+
label: "RPC Server Metrics",
70+
url: "https://opentelemetry.io/docs/specs/semconv/rpc/rpc-metrics/",
71+
},
72+
FAAS_SERVER_SPANS: {
73+
label: "FaaS Server Spans",
74+
url: "https://opentelemetry.io/docs/specs/semconv/faas/faas-spans/",
75+
},
76+
GRAPHQL_SERVER_SPANS: {
77+
label: "GraphQL Server Spans",
78+
url: "https://opentelemetry.io/docs/specs/semconv/graphql/graphql-spans/",
79+
},
80+
GENAI_CLIENT_SPANS: {
81+
label: "GenAI Client Spans",
82+
url: "https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/",
83+
},
84+
GENAI_CLIENT_METRICS: {
85+
label: "GenAI Client Metrics",
86+
url: "https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-metrics/",
87+
},
88+
};
89+
90+
export function getSemanticConventionInfo(value: string): SemanticConventionInfo | null {
91+
return SEMANTIC_CONVENTION_MAP[value] ?? null;
92+
}
93+
1894
export function getInstrumentationDisplayName(instrumentation: InstrumentationData): string {
1995
if (instrumentation.display_name) {
2096
return instrumentation.display_name;

0 commit comments

Comments
 (0)