Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update messaging for measuring message delay between App and Hub #2499

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
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 RegisterHostToAppPerformanceMetricsHandler = (): ReactElement =>
ApiWithoutInput({
name: 'registerHostToAppPerformanceMetricsHandler',
title: 'Register Host to App performance metrics handler',
onClick: async (setResult) => {
app.registerHostToAppPerformanceMetricsHandler((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 />
<RegisterHostToAppPerformanceMetricsHandler />
<RegisterOnThemeChangeHandler />
<RegisterBeforeSuspendOrTerminateHandler />
<RegisterOnResumeHandler />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Added new timestamp and handler for analyzing latencies due to message delays between app and hub.",
"packageName": "@microsoft/teams-js",
"email": "[email protected]",
"dependentChangeType": "patch"
}
13 changes: 11 additions & 2 deletions packages/teams-js/src/internal/communication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { latestRuntimeApiVersion } from '../public/runtime';
import { version } from '../public/version';
import { GlobalVars } from './globalVars';
import { callHandler } from './handlers';
import HostToAppMessageDelayTelemetry from './hostToAppTelemetry';
import { DOMMessageEvent, ExtendedWindow } from './interfaces';
import {
deserializeMessageRequest,
Expand All @@ -29,7 +30,7 @@ import {
tryPolyfillWithNestedAppAuthBridge,
} from './nestedAppAuthUtils';
import { getLogger, isFollowingApiVersionTagFormat } from './telemetry';
import { ssrSafeWindow } from './utils';
import { getCurrentTimestamp, ssrSafeWindow } from './utils';
import { UUID as MessageUUID } from './uuidObject';
import { validateOrigin } from './validOrigins';

Expand Down Expand Up @@ -158,6 +159,7 @@ export function uninitializeCommunication(): void {
CommunicationPrivate.promiseCallbacks.clear();
CommunicationPrivate.portCallbacks.clear();
CommunicationPrivate.legacyMessageIdsToUuidMap = {};
HostToAppMessageDelayTelemetry.clearMessages();
}

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

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

logger('Message %s information: %o', getMessageIdsAsLogString(request), { actionName, args });

Expand Down Expand Up @@ -715,6 +720,7 @@ function handleIncomingMessageFromParent(evt: DOMMessageEvent): void {
if (callbackId) {
const callback = CommunicationPrivate.callbacks.get(callbackId);
logger('Received a response from parent for message %s', callbackId.toString());
HostToAppMessageDelayTelemetry.handlePerformanceMetrics(callbackId, message, logger);
if (callback) {
logger(
'Invoking the registered callback for message %s with arguments %o',
Expand Down Expand Up @@ -766,6 +772,7 @@ function handleIncomingMessageFromParent(evt: DOMMessageEvent): void {
} else if ('func' in evt.data && typeof evt.data.func === 'string') {
// Delegate the request to the proper handler
const message = evt.data as MessageRequest;
HostToAppMessageDelayTelemetry.handleOneWayPerformanceMetrics(message, logger);
logger('Received a message from parent %s, action: "%s"', getMessageIdsAsLogString(message), message.func);
callHandler(message.func, message.args);
} else {
Expand Down Expand Up @@ -998,6 +1005,7 @@ function createMessageRequest(
uuid: messageUuid,
func: func,
timestamp: Date.now(),
monotonicTimestamp: getCurrentTimestamp(),
args: args || [],
apiVersionTag: apiVersionTag,
};
Expand All @@ -1023,6 +1031,7 @@ function createNestedAppAuthRequest(message: string): NestedAppAuthRequest {
uuid: messageUuid,
func: 'nestedAppAuth.execute',
timestamp: Date.now(),
monotonicTimestamp: getCurrentTimestamp(),
// Since this is a nested app auth request, we don't need to send any args.
// We avoid overloading the args array with the message to avoid potential issues processing of these messages on the hubSDK.
args: [],
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
90 changes: 90 additions & 0 deletions packages/teams-js/src/internal/hostToAppTelemetry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Debugger } from 'debug';

import { handleHostToAppPerformanceMetrics } from './handlers';
import { CallbackInformation } from './interfaces';
import { MessageRequest, MessageResponse } from './messageObjects';
import { getCurrentTimestamp } from './utils';
import { UUID as MessageUUID } from './uuidObject';

/**
* @internal
* Limited to Microsoft-internal use
*/
export default class HostToAppMessageDelayTelemetry {
private static callbackInformation: Map<MessageUUID, CallbackInformation> = new Map();

/**
* @internal
* Limited to Microsoft-internal use
*
* Store information about a particular message.
* @param messageUUID The message id for the request.
* @param callbackInformation The information of the callback.
*/
public static storeCallbackInformation(messageUUID: MessageUUID, callbackInformation: CallbackInformation): void {
HostToAppMessageDelayTelemetry.callbackInformation.set(messageUUID, callbackInformation);
}

/**
* @internal
* Limited to Microsoft-internal use
*/
public static clearMessages(): void {
HostToAppMessageDelayTelemetry.callbackInformation.clear();
}

/**
* @internal
* Limited to Microsoft-internal use
*/
public static deleteMessageInformation(callbackId: MessageUUID): void {
HostToAppMessageDelayTelemetry.callbackInformation.delete(callbackId);
}
/**
* @internal
* Limited to Microsoft-internal use
*
* Executes telemetry actions related to host to app performance metrics where event is raised in the host.
* @param message The request from the host.
* @param logger The logger in case an error occurs.
*/
public static handleOneWayPerformanceMetrics(message: MessageRequest, logger: Debugger): void {
const timestamp = message.monotonicTimestamp;
if (!timestamp) {
logger('Unable to send performance metrics for event %s', message.func);
return;
}
handleHostToAppPerformanceMetrics({
actionName: message.func,
messageDelay: getCurrentTimestamp() - timestamp,
messageWasCreatedAt: timestamp,
});
}

/**
* @internal
* Limited to Microsoft-internal use
*
* Executes telemetry actions related to host to app performance metrics.
* @param callbackId The message id for the request.
* @param message The response from the host.
* @param logger The logger in case an error occurs.
*/
public static handlePerformanceMetrics(callbackID: MessageUUID, message: MessageResponse, logger: Debugger): void {
const callbackInformation = HostToAppMessageDelayTelemetry.callbackInformation.get(callbackID);
if (!callbackInformation || !message.timestamp) {
logger(
'Unable to send performance metrics for callback %s with arguments %o',
callbackID.toString(),
message.args,
);
return;
}
handleHostToAppPerformanceMetrics({
actionName: callbackInformation.name,
messageDelay: getCurrentTimestamp() - message.timestamp,
messageWasCreatedAt: callbackInformation.calledAt,
});
HostToAppMessageDelayTelemetry.deleteMessageInformation(callbackID);
}
}
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.
juanscr marked this conversation as resolved.
Show resolved Hide resolved
*
* @internal
* Limited to Microsoft-internal use
*/
export interface CallbackInformation {
name: string;
calledAt: number;
}
6 changes: 6 additions & 0 deletions packages/teams-js/src/internal/messageObjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface MessageRequest {
uuid?: MessageUUID;
func: string;
timestamp?: number;
monotonicTimestamp?: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
args?: any[];
apiVersionTag?: string;
Expand All @@ -32,6 +33,7 @@ export interface SerializedMessageRequest {
uuidAsString?: string;
func: string;
timestamp?: number;
monotonicTimestamp?: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
args?: any[];
apiVersionTag?: string;
Expand All @@ -46,6 +48,7 @@ export interface SerializedMessageResponse {
uuidAsString?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
args?: any[];
timestamp?: number;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why wouldn't this have monotonicTimestamp?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only for the response that the Hosts sends to Teams JS. For requests, as Teams JS was already using the timestamp field we needed to create a new one for backwards compatibiltiy. For responses, we can use timestamp instead as it is a completely new field...

If you find it more homogeneous, we can still call it monotonicTimestamp for the response to comply with the field name in the request. But it wouldn't be necessary.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, pls use the same name to minimize confusion

isPartialResponse?: boolean; // If the message is partial, then there will be more future responses for the given message ID.
}

Expand All @@ -58,6 +61,7 @@ export interface MessageResponse {
uuid?: MessageUUID;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
args?: any[];
timestamp?: number;
juanscr marked this conversation as resolved.
Show resolved Hide resolved
isPartialResponse?: boolean; // If the message is partial, then there will be more future responses for the given message ID.
}

Expand All @@ -75,7 +79,9 @@ export interface MessageResponse {
export interface MessageRequestWithRequiredProperties extends MessageRequest {
id: MessageID;
uuid: MessageUUID;
/** Deprecated field, is still here for backwards compatibility */
timestamp: number;
monotonicTimestamp: number;
}

export const serializeMessageRequest = (message: MessageRequest): SerializedMessageRequest => {
Expand Down
14 changes: 14 additions & 0 deletions packages/teams-js/src/internal/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,3 +528,17 @@ export function validateUuid(id: string | undefined | null): void {
throw new Error('id must be a valid UUID');
}
}

/**
* Cache if performance timers are available to avoid redoing this on each function call.
*/
const supportsPerformanceTimers = !!performance && 'now' in performance;

/**
* @internal
* Limited to Microsoft-internal use
* @returns current timestamp in milliseconds
*/
export function getCurrentTimestamp(): number {
return supportsPerformanceTimers ? performance.now() + performance.timeOrigin : new Date().getTime();
juanscr marked this conversation as resolved.
Show resolved Hide resolved
}
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;
}
Loading