Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/web-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ jobs:
--archive=tgz \
--yes \
--token "$VERCEL_TOKEN" \
--scope "$vercel_scope"
--scope "$vercel_scope" \
--build-env "VITE_RELEASE_CHANNEL=preview"
)"
printf '%s\n' "$deployment_output"
# Vercel can print lockfile warnings to stdout before the deployment URL.
Expand Down
31 changes: 31 additions & 0 deletions apps/desktop/src/app/DesktopObservability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ describe("DesktopObservability", () => {
const body = requests[0]?.body ?? "";
assert.include(body, '"stringValue":"t3code-desktop"');
assert.include(body, '"key":"deployment.environment.name","value":{"stringValue":"staging"}');
assert.include(body, '"key":"t3code.release.channel","value":{"stringValue":"dev"}');
assert.include(body, '"key":"service.namespace","value":{"stringValue":"t3code"}');
assert.include(body, '"key":"process.runtime.name","value":{"stringValue":"nodejs"}');
assert.notInclude(body, "renamed");
Expand Down Expand Up @@ -358,6 +359,36 @@ describe("DesktopObservability", () => {
);
});

it.effect("reports the app's own release identity for a non-development run", () => {
const requests: Array<ExportedRequest> = [];
return Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-desktop-observability-test-",
});
const layerEnvironment = layerEnvironmentFor(baseDir, false);
yield* writeObservabilitySettings(layerEnvironment, {
otlpLogsUrl: "https://settings.example.com/v1/logs",
});

yield* Effect.scoped(
Effect.logInfo("desktop identity under test").pipe(
Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironment))),
),
);

const body = requests.map((request) => request.body).join("\n");
assert.include(
body,
'"key":"deployment.environment.name","value":{"stringValue":"production"}',
);
assert.include(body, '"key":"t3code.release.channel","value":{"stringValue":"stable"}');
}).pipe(
Effect.scoped,
Effect.provide(Layer.mergeAll(NodeServices.layer, layerCollector(requests), layerEmptyEnv)),
);
});

it.effect("reports the app's own version for an unpackaged desktop run", () => {
const requests: Array<ExportedRequest> = [];
return Effect.gen(function* () {
Expand Down
8 changes: 6 additions & 2 deletions apps/desktop/src/app/DesktopObservability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,13 +598,17 @@ const layerTelemetry = Layer.unwrap(
Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const endpoints = yield* resolveOtlpEndpoints;
// Unpackaged runs launch a script, so Electron reports its own version.
const serviceVersion = environment.isPackaged
? environment.appVersion
: desktopPackageJson.version;
const resource = {
serviceName: "t3code-desktop",
// Unpackaged runs launch a script, so Electron reports its own version.
serviceVersion: environment.isPackaged ? environment.appVersion : desktopPackageJson.version,
serviceVersion,
attributes: {
"service.namespace": "t3code",
...nodeProcessResourceAttributes({
version: serviceVersion,
operatorAttributes: endpoints.resourceAttributes,
isDevelopment: environment.isDevelopment,
}),
Expand Down
7 changes: 6 additions & 1 deletion apps/desktop/src/backend/DesktopBackendConfiguration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,7 @@ describe("DesktopBackendConfiguration", () => {
),
);

it.effect("resolveWsl carries the kill switch into the distro", () =>
it.effect("resolveWsl carries the kill switch and resource attributes into the distro", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
Expand All @@ -688,10 +688,12 @@ describe("DesktopBackendConfiguration", () => {
const previousWslEnv = process.env.WSLENV;
const previousDisabled = process.env.OTEL_SDK_DISABLED;
const previousTelemetry = process.env.T3CODE_TELEMETRY_ENABLED;
const previousResourceAttributes = process.env.OTEL_RESOURCE_ATTRIBUTES;
try {
delete process.env.WSLENV;
process.env.OTEL_SDK_DISABLED = "true";
process.env.T3CODE_TELEMETRY_ENABLED = "false";
process.env.OTEL_RESOURCE_ATTRIBUTES = "deployment.environment.name=staging";

yield* Effect.gen(function* () {
const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration;
Expand All @@ -701,6 +703,8 @@ describe("DesktopBackendConfiguration", () => {
assert.include((config.env.WSLENV ?? "").split(":"), "OTEL_SDK_DISABLED");
assert.equal(config.env.T3CODE_TELEMETRY_ENABLED, "false");
assert.include((config.env.WSLENV ?? "").split(":"), "T3CODE_TELEMETRY_ENABLED");
assert.equal(config.env.OTEL_RESOURCE_ATTRIBUTES, "deployment.environment.name=staging");
assert.include((config.env.WSLENV ?? "").split(":"), "OTEL_RESOURCE_ATTRIBUTES");
}).pipe(
Effect.provide(
layerBackendConfiguration().pipe(
Expand All @@ -722,6 +726,7 @@ describe("DesktopBackendConfiguration", () => {
restoreEnv("WSLENV", previousWslEnv);
restoreEnv("OTEL_SDK_DISABLED", previousDisabled);
restoreEnv("T3CODE_TELEMETRY_ENABLED", previousTelemetry);
restoreEnv("OTEL_RESOURCE_ATTRIBUTES", previousResourceAttributes);
}
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/backend/DesktopBackendConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ const WSL_FORWARDED_ENV_NAMES = [
"OTEL_TRACES_EXPORTER",
"OTEL_METRICS_EXPORTER",
"OTEL_LOGS_EXPORTER",
"OTEL_RESOURCE_ATTRIBUTES",
] as const;

const WSL_SERVER_SYSTEM_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
Expand Down
58 changes: 46 additions & 12 deletions apps/mobile/src/features/observability/tracing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ vi.mock("expo-constants", () => ({
},
}));

it.effect("labels exported spans with the mobile surface, environment, and instance", () => {
const exportedResourceBody = (appVariant: string) => {
const fetchFn = vi.fn<typeof fetch>(async () => new Response(null, { status: 202 }));
const layerTracing = Tracing.layerFromConfig(
{
Expand All @@ -26,7 +26,7 @@ it.effect("labels exported spans with the mobile surface, environment, and insta
tracesToken: "public-ingest-token",
},
{
appVariant: "test",
appVariant,
serviceInstanceId: "test-instance",
serviceVersion: "1.2.3",
},
Expand All @@ -40,18 +40,52 @@ it.effect("labels exported spans with the mobile surface, environment, and insta
Effect.andThen(
Effect.sync(() => {
expect(fetchFn).toHaveBeenCalledOnce();
const body = new TextDecoder().decode(fetchFn.mock.calls[0]?.[1]?.body as Uint8Array);
expect(body).toContain('"key":"t3code.client.surface","value":{"stringValue":"mobile"}');
expect(body).toContain(
'"key":"deployment.environment.name","value":{"stringValue":"test"}',
);
expect(body).toContain(
'"key":"service.instance.id","value":{"stringValue":"test-instance"}',
);
return new TextDecoder().decode(fetchFn.mock.calls[0]?.[1]?.body as Uint8Array);
}),
),
);
});
};

const stringAttribute = (key: string, value: string) =>
`"key":"${key}","value":{"stringValue":"${value}"}`;

it.effect("labels exported spans with the mobile surface and instance", () =>
exportedResourceBody("production").pipe(
Effect.andThen((body) =>
Effect.sync(() => {
expect(body).toContain(stringAttribute("t3code.client.surface", "mobile"));
expect(body).toContain(stringAttribute("service.instance.id", "test-instance"));
expect(body).toContain(stringAttribute("service.version", "1.2.3"));
}),
),
),
);

it.effect.each([
["development", "development", "dev"],
["preview", "staging", "preview"],
["production", "production", "stable"],
] as const)("reports %s builds as %s on the %s channel", ([appVariant, environment, channel]) =>
exportedResourceBody(appVariant).pipe(
Effect.andThen((body) =>
Effect.sync(() => {
expect(body).toContain(stringAttribute("deployment.environment.name", environment));
expect(body).toContain(stringAttribute("t3code.release.channel", channel));
}),
),
),
);

it.effect("omits environment and channel for an unknown variant", () =>
exportedResourceBody("unknown").pipe(
Effect.andThen((body) =>
Effect.sync(() => {
expect(body).not.toContain("deployment.environment.name");
expect(body).not.toContain("t3code.release.channel");
}),
),
),
);

it.effect("does not let OTLP serialization failures alter application effects", () => {
const fetchFn = vi.fn<typeof fetch>(async () => new Response(null, { status: 202 }));
Expand All @@ -62,7 +96,7 @@ it.effect("does not let OTLP serialization failures alter application effects",
tracesToken: "public-ingest-token",
},
{
appVariant: "test",
appVariant: "production",
serviceInstanceId: "test-instance",
serviceVersion: "1.2.3",
},
Expand Down
17 changes: 15 additions & 2 deletions apps/mobile/src/features/observability/tracing.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Constants from "expo-constants";
import type { ReleaseChannel } from "@t3tools/shared/releaseResource";
import * as RelayTracing from "@t3tools/shared/relayTracing";

import { hasTracingPublicConfig, resolveCloudPublicConfig } from "../cloud/publicConfig";
Expand All @@ -15,6 +16,14 @@ export interface TracingResource {
readonly appVariant: string;
}

const RELEASE_IDENTITY_BY_VARIANT: Readonly<
Record<string, { readonly environment: string; readonly channel: ReleaseChannel }>
> = {
development: { environment: "development", channel: "dev" },
preview: { environment: "staging", channel: "preview" },
production: { environment: "production", channel: "stable" },
};

export function resolveTracingConfig(): TracingConfig | null {
const config = resolveCloudPublicConfig();
if (!hasTracingPublicConfig(config)) {
Expand All @@ -25,15 +34,19 @@ export function resolveTracingConfig(): TracingConfig | null {
}

export function layerFromConfig(config: TracingConfig | null, resource: TracingResource) {
const releaseIdentity = Object.hasOwn(RELEASE_IDENTITY_BY_VARIANT, resource.appVariant)
? RELEASE_IDENTITY_BY_VARIANT[resource.appVariant]
: undefined;
return RelayTracing.layer(config, {
serviceName: "t3code-mobile",
serviceVersion: resource.serviceVersion,
serviceInstanceId: resource.serviceInstanceId,
client: "mobile",
attributes: {
"process.runtime.name": "react-native",
...(resource.appVariant !== "unknown" && {
"deployment.environment.name": resource.appVariant,
...(releaseIdentity && {
"deployment.environment.name": releaseIdentity.environment,
"t3code.release.channel": releaseIdentity.channel,
}),
},
});
Expand Down
19 changes: 19 additions & 0 deletions apps/server/src/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as OtelEnvironment from "@t3tools/shared/otelEnvironment";
import { assert, describe, it } from "@effect/vitest";

import packageJson from "../package.json" with { type: "json" };
import * as ServerConfig from "./config.ts";

describe("otlpResource", () => {
Expand All @@ -18,6 +19,24 @@ describe("otlpResource", () => {
"box-1",
);

it("reports the server's own version, release channel and environment", () => {
const attributes: Readonly<Record<string, unknown>> = resource({}).attributes;
assert.equal(resource({}).serviceVersion, packageJson.version);
assert.equal(attributes["t3code.release.channel"], "stable");
assert.equal(attributes["deployment.environment.name"], "production");

const dev: Readonly<Record<string, unknown>> = resource(
{},
{ devUrl: new URL("http://localhost:5733") },
).attributes;
assert.equal(dev["t3code.release.channel"], "dev");

const operator: Readonly<Record<string, unknown>> = resource({
"t3code.release.channel": "nightly",
}).attributes;
assert.equal(operator["t3code.release.channel"], "nightly");
});

it("reports who manages the server and its deployment environment", () => {
const standalone: Readonly<Record<string, unknown>> = resource({}).attributes;
assert.equal(standalone["t3code.server.managed_by"], "standalone");
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export const otlpResource = (
"service.namespace": "t3code",
"t3code.server.managed_by": config.mode === "desktop" ? "desktop" : "standalone",
...nodeProcessResourceAttributes({
version: packageJson.version,
operatorAttributes: config.otelEnvironment.resourceAttributes,
isDevelopment: config.devUrl !== undefined,
}),
Expand Down
46 changes: 46 additions & 0 deletions apps/web/src/observability/clientResource.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { describe, expect, it } from "vite-plus/test";

import { clientTracingResource } from "./clientResource";

const resource = (overrides: Partial<Parameters<typeof clientTracingResource>[0]> = {}) =>
clientTracingResource({
version: "1.2.3",
isDevelopment: false,
releaseChannel: undefined,
surface: "web",
serviceInstanceId: "instance-1",
navigator: undefined,
...overrides,
});

describe("clientTracingResource", () => {
it.each([
["a dev build", { isDevelopment: true }, "development", "dev"],
["a stable build", {}, "production", "stable"],
["an unrecognized channel", { releaseChannel: "unrecognized" }, "production", "stable"],
["a nightly build", { version: "1.2.3-nightly.20261023.4" }, "production", "nightly"],
["a PR preview", { releaseChannel: "preview" }, "staging", "preview"],
["a preview version", { version: "1.2.3-preview.20261023.1" }, "staging", "preview"],
] as const)("reports %s as %s on the %s channel", (_name, overrides, environment, channel) => {
const { attributes } = resource(overrides);
expect(attributes["deployment.environment.name"]).toBe(environment);
expect(attributes["t3code.release.channel"]).toBe(channel);
});

it("keeps the version untouched so the server version comparison still works", () => {
expect(resource({ releaseChannel: "preview" }).serviceVersion).toBe("1.2.3");
});

it("names the desktop window and the browser as separate surfaces", () => {
expect(resource({ surface: "desktop" }).attributes["t3code.client.surface"]).toBe("desktop");
expect(resource().attributes["t3code.client.surface"]).toBe("web");
});

it("describes the browser when one is present", () => {
const { attributes } = resource({
navigator: { userAgent: "UA", language: "en-US" },
});
expect(attributes["user_agent.original"]).toBe("UA");
expect(attributes["browser.language"]).toBe("en-US");
});
});
Loading
Loading