driver controller change (#3477)

* fix: launch app

* driver controller change
This commit is contained in:
Daniel Salazar
2026-07-30 00:41:29 -07:00
committed by GitHub
parent a22321daa2
commit 8a711e0254
3 changed files with 74 additions and 5 deletions
@@ -18,6 +18,7 @@
* [https://www.gnu.org/licenses/](https://www.gnu.org/licenses/).
*/
import { metrics } from '@opentelemetry/api';
import type { Request, Response } from 'express';
import { actorUid } from '../../core/actor.js';
import { Context } from '../../core/context.js';
@@ -45,6 +46,17 @@ import { PuterController } from '../types.js';
type DriverInstance = WithLifecycle & Record<string, unknown>;
// Every driver call is timed here already, for the lifecycle events below.
// Recording the same number as a histogram makes the per-interface latency
// distribution available downstream; which interfaces are worth keeping is a
// collector-side decision, not one made here, so this deliberately records
// everything and lets the export pipeline drop what it doesn't want.
const meter = metrics.getMeter('puter-backend');
const driverCallDuration = meter.createHistogram('driver.call.duration', {
description: 'Wall time of a driver method call',
unit: 'ms',
});
const extractUpstreamStatus = (e: {
status?: number;
statusCode?: number;
@@ -429,6 +441,11 @@ export class DriverController extends PuterController {
() => (fn as (...x: unknown[]) => any).call(driver, args),
);
} catch (e) {
driverCallDuration.record(Date.now() - startedAt, {
driver: ifaceName,
'driver.method': method,
outcome: 'error',
});
this.clients.event?.emit(
`driver.${ifaceName}.${method}.error`,
{
@@ -446,6 +463,15 @@ export class DriverController extends PuterController {
);
throw translateProviderError(e);
}
// Same window the span and the lifecycle events measure: for streamed
// results this is stream start, not stream drain. Worth remembering
// when reading AI latency — it is time-to-first-token, not total.
driverCallDuration.record(Date.now() - startedAt, {
driver: ifaceName,
'driver.method': method,
outcome: 'ok',
});
this.clients.event?.emit(
`driver.${ifaceName}.${method}.after`,
{
+18 -3
View File
@@ -241,8 +241,23 @@ window.Transaction = class {
this.attributes = { ...attributes };
}
/**
* Absolute, but read off the monotonic clock. A plain wall-clock read
* can jump mid-transaction when the system clock is adjusted, which
* lands as a negative or wildly inflated duration. Adding `timeOrigin`
* keeps the stamps on a real timeline (reporters place spans by
* absolute time) while the gap between two of them stays monotonic.
*/
#now () {
return typeof performance !== 'undefined'
&& typeof performance.now === 'function'
&& typeof performance.timeOrigin === 'number'
? performance.timeOrigin + performance.now()
: Date.now();
}
start () {
this.start_ts = Date.now();
this.start_ts = this.#now();
}
/**
@@ -254,11 +269,11 @@ window.Transaction = class {
}
getDuration () {
return Date.now() - this.start_ts;
return this.#now() - this.start_ts;
}
end () {
this.end_ts = Date.now();
this.end_ts = this.#now();
this.duration = this.end_ts - this.start_ts;
// emit an event
+30 -2
View File
@@ -116,6 +116,11 @@ const fetchUserAppTokenForLaunch = async ({ appUid } = {}) => {
*/
const launch_app = async (options) => {
let transaction;
// Ends once the app signals over IPC, i.e. when it can actually be used.
// Only apps built on the SDK ever signal, so this one is deliberately
// left unended (and therefore unreported) for the rest — better a series
// that covers fewer launches than one padded with timeouts.
let interactiveTransaction;
// A transaction to trace the time it takes to launch an app and
// for it to be ready.
// Explorer is a special case, it's not an app per se, so it doesn't need a transaction.
@@ -123,15 +128,31 @@ const launch_app = async (options) => {
// Attribute the timing: the same span covers a tile click on a warm
// dashboard and a cold landing on /app/<name>, which are different
// enough that a combined percentile describes neither.
transaction = new window.Transaction('app-is-ready', {
const launchAttributes = {
'launch.app': options?.name ?? options?.app_obj?.name ?? 'unknown',
'launch.dashboard_mode': !! window.is_dashboard_mode,
'launch.from_app_url':
typeof window.location?.pathname === 'string'
&& window.location.pathname.startsWith('/app/'),
'launch.has_app_obj': !! options?.app_obj,
};
// Exec-service launches never get the IPC listener attached below,
// so no interactive span is expected for them. Recording that here
// gives the interactive series a denominator: how many launches
// could have produced one.
const ipcTracked = ! options?.launched_by_exec_service;
transaction = new window.Transaction('app-is-ready', {
...launchAttributes,
'launch.ipc_tracked': ipcTracked,
});
transaction.start();
if ( ipcTracked ) {
interactiveTransaction = new window.Transaction(
'app-interactive', launchAttributes);
interactiveTransaction.start();
}
}
const uuid = options.uuid ?? window.uuidv4();
@@ -165,7 +186,9 @@ const launch_app = async (options) => {
// If no `options.name` is provided, use the app name from the app_info
options.name = options.name ?? app_info.name;
transaction?.annotate({ 'launch.app': options.name ?? 'unknown' });
const resolvedAppName = { 'launch.app': options.name ?? 'unknown' };
transaction?.annotate(resolvedAppName);
interactiveTransaction?.annotate(resolvedAppName);
const requestedAppName = options.privateLaunchRequestedAppName ?? options.name ?? app_info.name ?? null;
const privateAccessDecision = normalizePrivateAccessDecision(app_info.privateAccess);
@@ -737,6 +760,11 @@ const launch_app = async (options) => {
$(process.references.iframe).attr('data-appUsesSDK', 'true');
// The app is talking to us, so it's genuinely usable now —
// unlike `app-is-ready`, which stops at the window element.
endLaunchTransaction(interactiveTransaction, 'ipc-attached');
interactiveTransaction = undefined;
// Send any saved broadcasts to the new app
globalThis.services.get('broadcast').sendSavedBroadcastsTo(uuid);