Skip to content

Commit

Permalink
Create handler for streaming performance metrics.
Browse files Browse the repository at this point in the history
This new handler will allow any application developer to register
a function for analyzing or storing performance metrics related to
latencies due to a message delay sending the response back from the
host to the app.
  • Loading branch information
juanscr committed Sep 11, 2024
1 parent 50aa7e6 commit 2c32d66
Show file tree
Hide file tree
Showing 12 changed files with 259 additions and 10 deletions.
11 changes: 11 additions & 0 deletions apps/teams-test-app/src/components/AppAPIs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ const OpenLink = (): ReactElement =>
defaultInput: '"https://teams.microsoft.com/l/call/0/0?users=testUser1,testUser2&withVideo=true&source=test"',
});

const RegisterHubToAppPerformanceMetricsHandler = (): ReactElement =>
ApiWithoutInput({
name: 'registerHubToAppPerformanceMetricsHandler',
title: 'Register Hub to App performance metrics handler',
onClick: async (setResult) => {
app.registerHubToAppPerformanceMetricsHandler((v) => setResult(JSON.stringify(v)));
return '';
},
});

const RegisterOnThemeChangeHandler = (): ReactElement =>
ApiWithoutInput({
name: 'registerOnThemeChangeHandler',
Expand Down Expand Up @@ -128,6 +138,7 @@ const AppAPIs = (): ReactElement => (
<ModuleWrapper title="App">
<GetContext />
<OpenLink />
<RegisterHubToAppPerformanceMetricsHandler />
<RegisterOnThemeChangeHandler />
<RegisterBeforeSuspendOrTerminateHandler />
<RegisterOnResumeHandler />
Expand Down
24 changes: 21 additions & 3 deletions packages/teams-js/src/internal/communication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { SdkError } from '../public/interfaces';
import { latestRuntimeApiVersion } from '../public/runtime';
import { version } from '../public/version';
import { GlobalVars } from './globalVars';
import { callHandler } from './handlers';
import { DOMMessageEvent, ExtendedWindow } from './interfaces';
import { callHandler, handleHostToAppPerformanceMetrics } from './handlers';
import { CallbackInformation, DOMMessageEvent, ExtendedWindow } from './interfaces';
import {
deserializeMessageRequest,
deserializeMessageResponse,
Expand Down Expand Up @@ -59,6 +59,7 @@ class CommunicationPrivate {
public static topMessageQueue: MessageRequest[] = [];
public static nextMessageId = 0;
public static callbacks: Map<MessageUUID, Function> = new Map();
public static callbackInformation: Map<MessageUUID, CallbackInformation> = new Map();
public static promiseCallbacks: Map<MessageUUID, (value?: unknown) => void> = new Map();
public static portCallbacks: Map<MessageUUID, (port?: MessagePort, args?: unknown[]) => void> = new Map();
public static messageListener: Function;
Expand Down Expand Up @@ -158,6 +159,7 @@ export function uninitializeCommunication(): void {
CommunicationPrivate.promiseCallbacks.clear();
CommunicationPrivate.portCallbacks.clear();
CommunicationPrivate.legacyMessageIdsToUuidMap = {};
CommunicationPrivate.callbackInformation.clear();
}

/**
Expand Down Expand Up @@ -420,9 +422,12 @@ function sendMessageToParentHelper(
args: any[] | undefined,
): MessageRequestWithRequiredProperties {
const logger = sendMessageToParentHelperLogger;

const targetWindow = Communication.parentWindow;
const request = createMessageRequest(apiVersionTag, actionName, args);
CommunicationPrivate.callbackInformation.set(request.uuid, {
name: actionName,
calledAt: request.timestamp,
});

logger('Message %i information: %o', request.uuid, { actionName, args });

Expand Down Expand Up @@ -706,6 +711,19 @@ function handleParentMessage(evt: DOMMessageEvent): void {
if (callbackId) {
const callback = CommunicationPrivate.callbacks.get(callbackId);
logger('Received a response from parent for message %i', callbackId);

// Send performance metrics information of message delay
const callbackInformation = CommunicationPrivate.callbackInformation.get(callbackId);
if (callbackInformation && message.timestamp) {
handleHostToAppPerformanceMetrics({
actionName: callbackInformation.name,
messageDelay: getCurrentTimestamp() - message.timestamp,
messageWasCreatedAt: callbackInformation.calledAt,
});
} else {
logger('Unable to send performance metrics for callback %i with arguments %o', callbackId, message.args);
}

if (callback) {
logger('Invoking the registered callback for message %i with arguments %o', callbackId, message.args);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
Expand Down
24 changes: 23 additions & 1 deletion packages/teams-js/src/internal/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { ApiName, ApiVersionNumber, getApiVersionTag } from '../internal/telemetry';
import { FrameContexts } from '../public/constants';
import { LoadContext, ResumeContext } from '../public/interfaces';
import { HostToAppPerformanceMetrics, LoadContext, ResumeContext } from '../public/interfaces';
import { pages } from '../public/pages';
import { runtime } from '../public/runtime';
import { Communication, sendMessageEventToChild, sendMessageToParent } from './communication';
Expand Down Expand Up @@ -31,6 +31,7 @@ class HandlersPrivate {
public static beforeUnloadHandler: null | ((readyToUnload: () => void) => boolean) = null;
public static beforeSuspendOrTerminateHandler: null | (() => Promise<void>) = null;
public static resumeHandler: null | ((context: ResumeContext) => void) = null;
public static hostToAppPerformanceMetricsHandler: null | ((metrics: HostToAppPerformanceMetrics) => void) = null;

/**
* @internal
Expand Down Expand Up @@ -182,6 +183,27 @@ export function handleThemeChange(theme: string): void {
}
}

/**
* @internal
* Limited to Microsoft-internal use
*/
export function registerHostToAppPerformanceMetricsHandler(
handler: (metrics: HostToAppPerformanceMetrics) => void,
): void {
HandlersPrivate.hostToAppPerformanceMetricsHandler = handler;
}

/**
* @internal
* Limited to Microsoft-internal use
*/
export function handleHostToAppPerformanceMetrics(metrics: HostToAppPerformanceMetrics): void {
if (!HandlersPrivate.hostToAppPerformanceMetricsHandler) {
return;
}
HandlersPrivate.hostToAppPerformanceMetricsHandler(metrics);
}

/**
* @internal
* Limited to Microsoft-internal use
Expand Down
12 changes: 12 additions & 0 deletions packages/teams-js/src/internal/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,15 @@ export interface DOMMessageEvent {
func: string;
args?: any[];
}

/**
* @hidden
* Meant for providing information related to certain callback context.
*
* @internal
* Limited to Microsoft-internal use
*/
export interface CallbackInformation {
name: string;
calledAt: number;
}
1 change: 1 addition & 0 deletions packages/teams-js/src/internal/messageObjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface SerializedMessageResponse {
uuidAsString?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
args?: any[];
timestamp?: number;
isPartialResponse?: boolean; // If the message is partial, then there will be more future responses for the given message ID.
}

Expand Down
4 changes: 2 additions & 2 deletions packages/teams-js/src/internal/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,13 +532,13 @@ export function validateUuid(id: string | undefined | null): void {
/**
* Cache if performance timers are available to avoid redoing this on each function call.
*/
const supportsPerformanceTimers = 'performance' in window && 'now' in window.performance;
const supportsPerformanceTimers = !!performance && 'now' in performance;

/**
* @internal
* Limited to Microsoft-internal use
* @returns current timestamp in milliseconds
*/
export function getCurrentTimestamp(): number {
return supportsPerformanceTimers ? window.performance.now() + window.performance.timeOrigin : new Date().getTime();
return supportsPerformanceTimers ? performance.now() + performance.timeOrigin : new Date().getTime();
}
26 changes: 25 additions & 1 deletion packages/teams-js/src/public/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,14 @@ import { messageChannels } from '../private/messageChannels';
import { authentication } from './authentication';
import { ChannelType, FrameContexts, HostClientType, HostName, TeamType, UserTeamRole } from './constants';
import { dialog } from './dialog';
import { ActionInfo, Context as LegacyContext, FileOpenPreference, LocaleInfo, ResumeContext } from './interfaces';
import {
ActionInfo,
Context as LegacyContext,
FileOpenPreference,
HostToAppPerformanceMetrics,
LocaleInfo,
ResumeContext,
} from './interfaces';
import { menus } from './menus';
import { pages } from './pages';
import {
Expand Down Expand Up @@ -723,6 +730,11 @@ export namespace app {
*/
export type themeHandler = (theme: string) => void;

/**
* This function is passed to registerHostToAppPerformanceMetricsHandler. It is called every time a response is received from the host with metrics for analyzing message delay. See {@link HostToAppPerformanceMetrics} to see which metrics are passed to the handler.
*/
export type HostToAppPerformanceMetricsHandler = (metrics: HostToAppPerformanceMetrics) => void;

/**
* Checks whether the Teams client SDK has been initialized.
* @returns whether the Teams client SDK has been initialized.
Expand Down Expand Up @@ -886,6 +898,18 @@ export namespace app {
);
}

/**
* Registers a function for handling data of host to app message delay.
*
* @remarks
* Only one handler can be registered at a time. A subsequent registration replaces an existing registration.
*
* @param handler - The handler to invoke when the metrics are available on each function response.
*/
export function registerHostToAppPerformanceMetricsHandler(handler: HostToAppPerformanceMetricsHandler): void {
Handlers.registerHostToAppPerformanceMetricsHandler(handler);
}

/**
* This function opens deep links to other modules in the host such as chats or channels or
* general-purpose links (to external websites). It should not be used for navigating to your
Expand Down
1 change: 1 addition & 0 deletions packages/teams-js/src/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export {
FileOpenPreference,
FrameContext,
FrameInfo,
HostToAppPerformanceMetrics,
LoadContext,
LocaleInfo,
M365ContentAction,
Expand Down
14 changes: 13 additions & 1 deletion packages/teams-js/src/public/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1008,7 +1008,7 @@ export interface SdkError {
errorCode: ErrorCode;
/**
Optional description for the error. This may contain useful information for web-app developers.
This string will not be localized and is not for end-user consumption.
This string will not be localized and is not for end-user consumption.
App should not depend on the string content. The exact value may change. This is only for debugging purposes.
*/
message?: string;
Expand Down Expand Up @@ -1272,3 +1272,15 @@ export interface ClipboardParams {
/** Blob content in Base64 string format */
content: string;
}

/**
* Meant for passing data to the app related to host-to-app message performance metrics.
*/
export interface HostToAppPerformanceMetrics {
/** The name of the action the host is responding to. */
actionName: string;
/** The delay the message took traveling from host to app */
messageDelay: number;
/** The time the message was originally created at */
messageWasCreatedAt: number;
}
3 changes: 2 additions & 1 deletion packages/teams-js/test/internal/communication.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Utils } from '../utils';

jest.mock('../../src/internal/handlers', () => ({
callHandler: jest.fn(),
handleHostToAppPerformanceMetrics: jest.fn(),
}));

const testApiVersion = getApiVersionTag(ApiVersionNumber.V_1, 'mockedApiName' as ApiName);
Expand Down Expand Up @@ -275,7 +276,7 @@ describe('Testing communication', () => {
it('should set Communication.parentWindow and Communication.parentOrigin to null if the parent window is closed during the initialization call', async () => {
expect.assertions(4);

/*
/*
This promise is intentionally not being awaited
If the parent window is closed during the initialize call,
the initialize response never resolves (even though we receive it)
Expand Down
Loading

0 comments on commit 2c32d66

Please sign in to comment.