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
2 changes: 0 additions & 2 deletions apps/dev-playground/client/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import path from "node:path";
import { appKitTypesPlugin } from "@databricks/app-kit";
import { tanstackRouter } from "@tanstack/router-plugin/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
Expand All @@ -8,7 +7,6 @@ import { defineConfig } from "vite";
export default defineConfig({
plugins: [
react(),
appKitTypesPlugin(),
tanstackRouter({
target: "react",
autoCodeSplitting: process.env.NODE_ENV !== "development",
Expand Down
2 changes: 2 additions & 0 deletions apps/dev-playground/server/reconnect-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export class ReconnectPlugin extends Plugin {

injectRoutes(router: IAppRouter): void {
this.route<ReconnectResponse>(router, {
name: "reconnect",
method: "get",
path: "/",
handler: async (_req, res) => {
Expand All @@ -27,6 +28,7 @@ export class ReconnectPlugin extends Plugin {
});

this.route<ReconnectStreamResponse>(router, {
name: "stream",
method: "get",
path: "/stream",
handler: async (req, res) => {
Expand Down
229 changes: 118 additions & 111 deletions apps/dev-playground/server/telemetry-example-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,118 +41,125 @@ class TelemetryExamples extends Plugin {
}

private registerTelemetryExampleRoutes(router: Router) {
router.post("/combined", async (req: Request, res: Response) => {
const startTime = Date.now();

return this.telemetry.startActiveSpan(
"combined-example",
{
attributes: {
"example.type": "combined",
"example.version": "v2",
this.route(router, {
name: "combined",
method: "post",
path: "/combined",
handler: async (req: Request, res: Response) => {
const startTime = Date.now();

return this.telemetry.startActiveSpan(
"combined-example",
{
attributes: {
"example.type": "combined",
"example.version": "v2",
},
},
},
async (span: Span) => {
try {
const userId =
req.body?.userId || req.query.userId || "demo-user-123";

this.telemetry.emit({
severityNumber: SeverityNumber.INFO,
severityText: "INFO",
body: "Processing telemetry example request",
attributes: {
"user.id": userId,
"request.type": "combined-example",
},
});

const result = await this.complexOperation(userId);

const duration = Date.now() - startTime;
this.requestCounter.add(1, { status: "success" });
this.durationHistogram.record(duration);

this.telemetry.emit({
severityNumber: SeverityNumber.INFO,
severityText: "INFO",
body: "Request completed successfully",
attributes: {
"user.id": userId,
"duration.ms": duration,
"result.fields": Object.keys(result).length,
},
});

span.setStatus({ code: SpanStatusCode.OK });

res.json({
success: true,
result,
duration_ms: duration,
tracing: {
hint: "Open Grafana at http://localhost:3000",
services: [
"app-template (main service)",
"user-operations (complex operation)",
"auth-validation (user validation)",
"data-access (database operations - with cache!)",
"auth-service (permissions)",
"external-api (external HTTP calls)",
"data-processing (transformation)",
],
expectedSpans: [
"HTTP POST (SDK auto-instrumentation)",
"combined-example (custom tracer: custom-telemetry-example)",
" └─ complex-operation (custom tracer: user-operations)",
" ├─ validate-user (100ms, custom tracer: auth-validation)",
" ├─ fetch-user-data (200ms first call / cached on repeat, custom tracer: data-access) [parallel]",
" │ └─ cache.hit attribute set by SDK (false on first call, true on repeat)",
" ├─ fetch-external-resource (custom tracer: external-api) [parallel]",
" │ └─ HTTP GET https://example.com (SDK auto-instrumentation)",
" ├─ fetch-permissions (150ms, custom tracer: auth-service) [parallel]",
" └─ transform-data (80ms, custom tracer: data-processing)",
],
},
metrics: {
recorded: ["app.requests.total", "app.request.duration"],
},
logs: {
emitted: [
"Starting complex operation workflow",
"Data fetching completed successfully",
"Data transformation completed",
"Permissions retrieved",
"External API call completed",
],
},
});
} catch (error) {
span.recordException(error as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
this.requestCounter.add(1, { status: "error" });

this.telemetry.emit({
severityNumber: SeverityNumber.ERROR,
severityText: "ERROR",
body: error instanceof Error ? error.message : "Unknown error",
attributes: {
"error.type": error instanceof Error ? error.name : "Unknown",
"error.stack": error instanceof Error ? error.stack : undefined,
"request.path": req.path,
},
});

res.status(500).json({
error: true,
message: error instanceof Error ? error.message : "Unknown error",
});
} finally {
span.end();
}
},
{ name: "custom-telemetry-example" },
);
async (span: Span) => {
try {
const userId =
req.body?.userId || req.query.userId || "demo-user-123";

this.telemetry.emit({
severityNumber: SeverityNumber.INFO,
severityText: "INFO",
body: "Processing telemetry example request",
attributes: {
"user.id": userId,
"request.type": "combined-example",
},
});

const result = await this.complexOperation(userId);

const duration = Date.now() - startTime;
this.requestCounter.add(1, { status: "success" });
this.durationHistogram.record(duration);

this.telemetry.emit({
severityNumber: SeverityNumber.INFO,
severityText: "INFO",
body: "Request completed successfully",
attributes: {
"user.id": userId,
"duration.ms": duration,
"result.fields": Object.keys(result).length,
},
});

span.setStatus({ code: SpanStatusCode.OK });

res.json({
success: true,
result,
duration_ms: duration,
tracing: {
hint: "Open Grafana at http://localhost:3000",
services: [
"app-template (main service)",
"user-operations (complex operation)",
"auth-validation (user validation)",
"data-access (database operations - with cache!)",
"auth-service (permissions)",
"external-api (external HTTP calls)",
"data-processing (transformation)",
],
expectedSpans: [
"HTTP POST (SDK auto-instrumentation)",
"combined-example (custom tracer: custom-telemetry-example)",
" └─ complex-operation (custom tracer: user-operations)",
" ├─ validate-user (100ms, custom tracer: auth-validation)",
" ├─ fetch-user-data (200ms first call / cached on repeat, custom tracer: data-access) [parallel]",
" │ └─ cache.hit attribute set by SDK (false on first call, true on repeat)",
" ├─ fetch-external-resource (custom tracer: external-api) [parallel]",
" │ └─ HTTP GET https://example.com (SDK auto-instrumentation)",
" ├─ fetch-permissions (150ms, custom tracer: auth-service) [parallel]",
" └─ transform-data (80ms, custom tracer: data-processing)",
],
},
metrics: {
recorded: ["app.requests.total", "app.request.duration"],
},
logs: {
emitted: [
"Starting complex operation workflow",
"Data fetching completed successfully",
"Data transformation completed",
"Permissions retrieved",
"External API call completed",
],
},
});
} catch (error) {
span.recordException(error as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
this.requestCounter.add(1, { status: "error" });

this.telemetry.emit({
severityNumber: SeverityNumber.ERROR,
severityText: "ERROR",
body: error instanceof Error ? error.message : "Unknown error",
attributes: {
"error.type": error instanceof Error ? error.name : "Unknown",
"error.stack":
error instanceof Error ? error.stack : undefined,
"request.path": req.path,
},
});

res.status(500).json({
error: true,
message:
error instanceof Error ? error.message : "Unknown error",
});
} finally {
span.end();
}
},
{ name: "custom-telemetry-example" },
);
},
});
}

Expand Down
4 changes: 4 additions & 0 deletions packages/app-kit/src/analytics/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export class AnalyticsPlugin extends Plugin {

injectRoutes(router: IAppRouter) {
this.route(router, {
name: "arrow",
method: "get",
path: "/arrow-result/:jobId",
handler: async (req: Request, res: Response) => {
Expand All @@ -50,6 +51,7 @@ export class AnalyticsPlugin extends Plugin {
});

this.route(router, {
name: "arrowAsUser",
method: "get",
path: "/users/me/arrow-result/:jobId",
handler: async (req: Request, res: Response) => {
Expand All @@ -58,6 +60,7 @@ export class AnalyticsPlugin extends Plugin {
});

this.route<AnalyticsQueryResponse>(router, {
name: "queryAsUser",
method: "post",
path: "/users/me/query/:query_key",
handler: async (req: Request, res: Response) => {
Expand All @@ -66,6 +69,7 @@ export class AnalyticsPlugin extends Plugin {
});

this.route<AnalyticsQueryResponse>(router, {
name: "query",
method: "post",
path: "/query/:query_key",
handler: async (req: Request, res: Response) => {
Expand Down
20 changes: 20 additions & 0 deletions packages/app-kit/src/core/tests/databricks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ class CoreTestPlugin implements BasePlugin {
asUser() {
return this;
}

getEndpoints() {
return {};
}
}

class NormalTestPlugin implements BasePlugin {
Expand Down Expand Up @@ -80,6 +84,10 @@ class NormalTestPlugin implements BasePlugin {
asUser() {
return this;
}

getEndpoints() {
return {};
}
}

class DeferredTestPlugin implements BasePlugin {
Expand Down Expand Up @@ -109,6 +117,10 @@ class DeferredTestPlugin implements BasePlugin {
asUser(): any {
return this;
}

getEndpoints() {
return {};
}
}

class SlowSetupPlugin implements BasePlugin {
Expand All @@ -133,6 +145,10 @@ class SlowSetupPlugin implements BasePlugin {
asUser(): any {
return this;
}

getEndpoints() {
return {};
}
}

class FailingPlugin implements BasePlugin {
Expand All @@ -152,6 +168,10 @@ class FailingPlugin implements BasePlugin {
asUser(): any {
return this;
}

getEndpoints() {
return {};
}
}

describe("AppKit", () => {
Expand Down
Loading