Skip to content

Commit 7ce89ca

Browse files
committed
fix(analytics): filter browser traces before forwarding
- keep only marcode.* spans in product analytics exports - add regression coverage for generic browser traces - include a grafana dashboard for marcode product analytics
1 parent 9b2d0fa commit 7ce89ca

3 files changed

Lines changed: 1543 additions & 3 deletions

File tree

apps/server/src/http.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,21 @@ function appendResourceAttributes(
116116
};
117117
}
118118

119+
export function filterProductAnalyticsTracePayload(
120+
bodyJson: OtlpTracer.TraceData,
121+
): OtlpTracer.TraceData {
122+
return {
123+
...bodyJson,
124+
resourceSpans: bodyJson.resourceSpans.flatMap((resourceSpan) => {
125+
const scopeSpans = resourceSpan.scopeSpans.flatMap((scopeSpan) => {
126+
const spans = scopeSpan.spans.filter((span) => span.name.startsWith("marcode."));
127+
return spans.length > 0 ? [{ ...scopeSpan, spans }] : [];
128+
});
129+
return scopeSpans.length > 0 ? [{ ...resourceSpan, scopeSpans }] : [];
130+
}),
131+
};
132+
}
133+
119134
const getJiraProofHeader = (targetUrl: string) =>
120135
Effect.gen(function* () {
121136
const config = yield* ServerConfig;
@@ -136,9 +151,12 @@ const forwardProductAnalyticsTraces = (bodyJson: OtlpTracer.TraceData) =>
136151
const productAnalyticsUrl = productAnalyticsUrlFromConfig(config);
137152
if (!productAnalyticsUrl) return;
138153

154+
const productBodyJson = filterProductAnalyticsTracePayload(bodyJson);
155+
if (productBodyJson.resourceSpans.length === 0) return;
156+
139157
const identifier = yield* getTelemetryIdentifier;
140158
const proofHeader = yield* getJiraProofHeader(productAnalyticsUrl);
141-
const enrichedBodyJson = appendResourceAttributes(bodyJson, {
159+
const enrichedBodyJson = appendResourceAttributes(productBodyJson, {
142160
...(identifier
143161
? {
144162
"analytics.user.id": identifier.id,

apps/server/src/server.test.ts

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1737,14 +1737,14 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
17371737
);
17381738

17391739
it.effect(
1740-
"forwards browser OTLP traces to product analytics with Jira proof only for trusted origin",
1740+
"forwards browser product analytics spans with Jira proof only for trusted origin",
17411741
() =>
17421742
Effect.gen(function* () {
17431743
const productRequests: Array<{
17441744
readonly body: string;
17451745
readonly jiraToken: string | null;
17461746
}> = [];
1747-
const payload = yield* makeBrowserOtlpPayload("product.client.test");
1747+
const payload = yield* makeBrowserOtlpPayload("marcode.ui.composer.submit");
17481748
const collector = yield* Effect.acquireRelease(
17491749
Effect.promise(async () => {
17501750
const NodeHttp = await import("node:http");
@@ -1813,11 +1813,68 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
18131813
assert.equal(productRequests.length, 1);
18141814
assert.equal(productRequests[0]?.jiraToken, "jira-proof-token");
18151815
const forwarded = JSON.parse(productRequests[0]!.body) as typeof payload;
1816+
const forwardedSpan = forwarded.resourceSpans[0]?.scopeSpans[0]?.spans[0]?.name ?? "";
1817+
assert.equal(forwardedSpan, "marcode.ui.composer.submit");
18161818
const attributes = forwarded.resourceSpans[0]?.resource?.attributes ?? [];
18171819
assertTrue(attributes.some((attribute) => attribute.key === "analytics.user.is_genesis"));
18181820
}).pipe(Effect.provide(NodeHttpServer.layerTest)),
18191821
);
18201822

1823+
it.effect("does not forward generic browser observability traces to product analytics", () =>
1824+
Effect.gen(function* () {
1825+
let productRequestCount = 0;
1826+
const payload = yield* makeBrowserOtlpPayload("RpcClient.git.listBranches");
1827+
const collector = yield* Effect.acquireRelease(
1828+
Effect.promise(async () => {
1829+
const NodeHttp = await import("node:http");
1830+
return await new Promise<{
1831+
readonly close: () => Promise<void>;
1832+
readonly url: string;
1833+
}>((resolve, reject) => {
1834+
const server = NodeHttp.createServer((_request, response) => {
1835+
productRequestCount++;
1836+
response.statusCode = 204;
1837+
response.end();
1838+
});
1839+
server.on("error", reject);
1840+
server.listen(0, "127.0.0.1", () => {
1841+
const address = server.address();
1842+
if (!address || typeof address === "string") {
1843+
reject(new Error("Expected TCP collector address"));
1844+
return;
1845+
}
1846+
resolve({
1847+
url: `http://127.0.0.1:${address.port}/api/otel/traces`,
1848+
close: () =>
1849+
new Promise<void>((resolveClose, rejectClose) => {
1850+
server.close((error) => (error ? rejectClose(error) : resolveClose()));
1851+
}),
1852+
});
1853+
});
1854+
});
1855+
}),
1856+
({ close }) => Effect.promise(close),
1857+
);
1858+
1859+
yield* buildAppUnderTest({
1860+
config: {
1861+
productAnalyticsTracesUrl: collector.url,
1862+
},
1863+
});
1864+
1865+
const response = yield* HttpClient.post("/api/observability/v1/traces", {
1866+
headers: {
1867+
cookie: yield* getAuthenticatedSessionCookieHeader(),
1868+
"content-type": "application/json",
1869+
},
1870+
body: HttpBody.text(JSON.stringify(payload), "application/json"),
1871+
});
1872+
1873+
assert.equal(response.status, 204);
1874+
assert.equal(productRequestCount, 0);
1875+
}).pipe(Effect.provide(NodeHttpServer.layerTest)),
1876+
);
1877+
18211878
it.effect(
18221879
"stores browser OTLP trace exports locally when no upstream collector is configured",
18231880
() =>

0 commit comments

Comments
 (0)