Skip to content

Commit 1ff9a19

Browse files
authored
Wire unhandled widget messages to the jupyter output (microsoft#11273)
1 parent 59b1a81 commit 1ff9a19

File tree

11 files changed

+73
-8
lines changed

11 files changed

+73
-8
lines changed

news/2 Fixes/11239.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Show unhandled widget messages in the jupyter output window.

package.nls.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,5 +473,6 @@
473473
"DataScience.useCDNForWidgets": "Widgets require us to download supporting files from a 3rd party website. Click [here](https://aka.ms/PVSCIPyWidgets) for more information.",
474474
"DataScience.loadThirdPartyWidgetScriptsPostEnabled": "Please restart the Kernel when changing the setting 'python.dataScience.widgetScriptSources'.",
475475
"DataScience.enableCDNForWidgetsSetting": "Widgets require us to download supporting files from a 3rd party website. Click <a href='https://command:python.datascience.enableLoadingWidgetScriptsFromThirdPartySource'>here</a> to enable this or click <a href='https://aka.ms/PVSCIPyWidgets'>here</a> for more information. (Error loading {0}:{1}).",
476-
"DataScience.widgetScriptNotFoundOnCDNWidgetMightNotWork": "Unable to load a compatible version of the widget '{0}'. Expected behavior may be affected."
476+
"DataScience.widgetScriptNotFoundOnCDNWidgetMightNotWork": "Unable to load a compatible version of the widget '{0}'. Expected behavior may be affected.",
477+
"DataScience.unhandledMessage": "Unhandled kernel message from a widget: {0} : {1}"
477478
}

src/client/common/utils/localize.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,12 @@ export namespace DataScience {
866866
'DataScience.enableCDNForWidgetsSetting',
867867
"Widgets require us to download supporting files from a 3rd party website. Click <a href='https://command:python.datascience.enableLoadingWidgetScriptsFromThirdPartySource'>here</a> to enable this or click <a href='https://aka.ms/PVSCIPyWidgets'>here</a> for more information. (Error loading {0}:{1})."
868868
);
869+
870+
export const unhandledMessage = localize(
871+
'DataScience.unhandledMessage',
872+
'Unhandled kernel message from a widget: {0} : {1}'
873+
);
874+
869875
export const widgetScriptNotFoundOnCDNWidgetMightNotWork = localize(
870876
'DataScience.widgetScriptNotFoundOnCDNWidgetMightNotWork',
871877
"Unable to load a compatible version of the widget '{0}'. Expected behavior may be affected."

src/client/datascience/constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,8 @@ export enum Telemetry {
299299
IPyWidgetPromptToUseCDN = 'DS_INTERNAL.IPYWIDGET_PROMPT_TO_USE_CDN',
300300
IPyWidgetPromptToUseCDNSelection = 'DS_INTERNAL.IPYWIDGET_PROMPT_TO_USE_CDN_SELECTION',
301301
IPyWidgetOverhead = 'DS_INTERNAL.IPYWIDGET_OVERHEAD',
302-
IPyWidgetRenderFailure = 'DS_INTERNAL.IPYWIDGET_RENDER_FAILURE'
302+
IPyWidgetRenderFailure = 'DS_INTERNAL.IPYWIDGET_RENDER_FAILURE',
303+
IPyWidgetUnhandledMessage = 'DS_INTERNAL.IPYWIDGET_UNHANDLED_MESSAGE'
303304
}
304305

305306
export enum NativeKeyboardCommandTelemetry {

src/client/datascience/interactive-common/interactiveWindowTypes.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ export enum InteractiveWindowMessages {
116116
UpdateDisplayData = 'update_display_data',
117117
IPyWidgetLoadSuccess = 'ipywidget_load_success',
118118
IPyWidgetLoadFailure = 'ipywidget_load_failure',
119-
IPyWidgetRenderFailure = 'ipywidget_render_failure'
119+
IPyWidgetRenderFailure = 'ipywidget_render_failure',
120+
IPyWidgetUnhandledKernelMessage = 'ipywidget_unhandled_kernel_message'
120121
}
121122

122123
export enum IPyWidgetMessages {
@@ -582,4 +583,5 @@ export class IInteractiveWindowMapping {
582583
public [InteractiveWindowMessages.ConvertUriForUseInWebViewRequest]: Uri;
583584
public [InteractiveWindowMessages.ConvertUriForUseInWebViewResponse]: { request: Uri; response: Uri };
584585
public [InteractiveWindowMessages.IPyWidgetRenderFailure]: Error;
586+
public [InteractiveWindowMessages.IPyWidgetUnhandledKernelMessage]: KernelMessage.IMessage;
585587
}

src/client/datascience/interactive-common/synchronization.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ const messageWithMessageTypes: MessageMapping<IInteractiveWindowMapping> & Messa
118118
[InteractiveWindowMessages.IPyWidgetLoadSuccess]: MessageType.other,
119119
[InteractiveWindowMessages.IPyWidgetLoadFailure]: MessageType.other,
120120
[InteractiveWindowMessages.IPyWidgetRenderFailure]: MessageType.other,
121+
[InteractiveWindowMessages.IPyWidgetUnhandledKernelMessage]: MessageType.other,
121122
[InteractiveWindowMessages.LoadAllCells]: MessageType.other,
122123
[InteractiveWindowMessages.LoadAllCellsComplete]: MessageType.other,
123124
[InteractiveWindowMessages.LoadOnigasmAssemblyRequest]: MessageType.other,

src/client/datascience/ipywidgets/ipywidgetHandler.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,20 @@
33

44
'use strict';
55

6-
import { inject, injectable } from 'inversify';
6+
import type { KernelMessage } from '@jupyterlab/services';
7+
import { inject, injectable, named } from 'inversify';
8+
import stripAnsi from 'strip-ansi';
79
import { Event, EventEmitter, Uri } from 'vscode';
810
import {
911
ILoadIPyWidgetClassFailureAction,
1012
LoadIPyWidgetClassLoadAction
1113
} from '../../../datascience-ui/interactive-common/redux/reducers/types';
1214
import { EnableIPyWidgets } from '../../common/experimentGroups';
13-
import { traceError } from '../../common/logger';
14-
import { IDisposableRegistry, IExperimentsManager } from '../../common/types';
15+
import { traceError, traceInfo } from '../../common/logger';
16+
import { IDisposableRegistry, IExperimentsManager, IOutputChannel } from '../../common/types';
17+
import * as localize from '../../common/utils/localize';
1518
import { sendTelemetryEvent } from '../../telemetry';
16-
import { Telemetry } from '../constants';
19+
import { JUPYTER_OUTPUT_CHANNEL, Telemetry } from '../constants';
1720
import { INotebookIdentity, InteractiveWindowMessages } from '../interactive-common/interactiveWindowTypes';
1821
import { IInteractiveWindowListener, INotebookProvider } from '../types';
1922
import { IPyWidgetMessageDispatcherFactory } from './ipyWidgetMessageDispatcherFactory';
@@ -46,7 +49,8 @@ export class IPyWidgetHandler implements IInteractiveWindowListener {
4649
@inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry,
4750
@inject(IPyWidgetMessageDispatcherFactory)
4851
private readonly widgetMessageDispatcherFactory: IPyWidgetMessageDispatcherFactory,
49-
@inject(IExperimentsManager) readonly experimentsManager: IExperimentsManager
52+
@inject(IExperimentsManager) readonly experimentsManager: IExperimentsManager,
53+
@inject(IOutputChannel) @named(JUPYTER_OUTPUT_CHANNEL) private jupyterOutput: IOutputChannel
5054
) {
5155
disposables.push(
5256
notebookProvider.onNotebookCreated(async (e) => {
@@ -73,6 +77,8 @@ export class IPyWidgetHandler implements IInteractiveWindowListener {
7377
this.sendLoadFailureTelemetry(payload);
7478
} else if (message === InteractiveWindowMessages.IPyWidgetRenderFailure) {
7579
this.sendRenderFailureTelemetry(payload);
80+
} else if (message === InteractiveWindowMessages.IPyWidgetUnhandledKernelMessage) {
81+
this.handleUnhandledMessage(payload);
7682
}
7783
// tslint:disable-next-line: no-any
7884
this.getIPyWidgetMessageDispatcher()?.receiveMessage({ message: message as any, payload }); // NOSONAR
@@ -112,6 +118,26 @@ export class IPyWidgetHandler implements IInteractiveWindowListener {
112118
// Do nothing on a failure
113119
}
114120
}
121+
122+
private handleUnhandledMessage(msg: KernelMessage.IMessage) {
123+
// Skip status messages
124+
if (msg.header.msg_type !== 'status') {
125+
try {
126+
// Special case errors, strip ansi codes from tracebacks so they print better.
127+
if (msg.header.msg_type === 'error') {
128+
const errorMsg = msg as KernelMessage.IErrorMsg;
129+
errorMsg.content.traceback = errorMsg.content.traceback.map(stripAnsi);
130+
}
131+
traceInfo(`Unhandled widget kernel message: ${msg.header.msg_type} ${msg.content}`);
132+
this.jupyterOutput.appendLine(
133+
localize.DataScience.unhandledMessage().format(msg.header.msg_type, JSON.stringify(msg.content))
134+
);
135+
sendTelemetryEvent(Telemetry.IPyWidgetUnhandledMessage, undefined, { msg_type: msg.header.msg_type });
136+
} catch {
137+
// Don't care if this doesn't get logged
138+
}
139+
}
140+
}
115141
private getIPyWidgetMessageDispatcher() {
116142
if (!this.notebookIdentity || !this.enabled) {
117143
return;

src/client/telemetry/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2005,4 +2005,10 @@ export interface IEventNamePropertyMapping {
20052005
* Telemetry event sent when the widget render function fails (note, this may not be sufficient to capture all failures).
20062006
*/
20072007
[Telemetry.IPyWidgetRenderFailure]: never | undefined;
2008+
/**
2009+
* Telemetry event sent when the widget tries to send a kernel message but nothing was listening
2010+
*/
2011+
[Telemetry.IPyWidgetUnhandledMessage]: {
2012+
msg_type: string;
2013+
};
20082014
}

src/datascience-ui/ipywidgets/manager.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { ReplaySubject } from 'rxjs/ReplaySubject';
1515
import { createDeferred, Deferred } from '../../client/common/utils/async';
1616
import {
1717
IInteractiveWindowMapping,
18+
InteractiveWindowMessages,
1819
IPyWidgetMessages
1920
} from '../../client/datascience/interactive-common/interactiveWindowTypes';
2021
import { KernelSocketOptions } from '../../client/datascience/types';
@@ -161,6 +162,9 @@ export class WidgetManager implements IIPyWidgetManager, IMessageHandler {
161162
// Listen for display data messages so we can prime the model for a display data
162163
this.proxyKernel.iopubMessage.connect(this.handleDisplayDataMessage.bind(this));
163164

165+
// Listen for unhandled IO pub so we can forward to the extension
166+
this.manager.onUnhandledIOPubMessage.connect(this.handleUnhanldedIOPubMessage.bind(this));
167+
164168
// Tell the observable about our new manager
165169
WidgetManager._instance.next(this);
166170
} catch (ex) {
@@ -204,4 +208,12 @@ export class WidgetManager implements IIPyWidgetManager, IMessageHandler {
204208
}
205209
}
206210
}
211+
212+
private handleUnhanldedIOPubMessage(_manager: any, msg: KernelMessage.IIOPubMessage) {
213+
// Send this to the other side
214+
this.postOffice.sendMessage<IInteractiveWindowMapping>(
215+
InteractiveWindowMessages.IPyWidgetUnhandledKernelMessage,
216+
msg
217+
);
218+
}
207219
}

src/datascience-ui/ipywidgets/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import * as jupyterlab from '@jupyter-widgets/base/lib';
77
import type { Kernel, KernelMessage } from '@jupyterlab/services';
88
import type { nbformat } from '@jupyterlab/services/node_modules/@jupyterlab/coreutils';
9+
import { ISignal } from '@phosphor/signaling';
910
import { Widget } from '@phosphor/widgets';
1011
import { IInteractiveWindowMapping } from '../../client/datascience/interactive-common/interactiveWindowTypes';
1112

@@ -25,6 +26,10 @@ export type IJupyterLabWidgetManagerCtor = new (
2526
) => IJupyterLabWidgetManager;
2627

2728
export interface IJupyterLabWidgetManager {
29+
/**
30+
* Signal emitted when a view emits an IO Pub message but nothing handles it.
31+
*/
32+
readonly onUnhandledIOPubMessage: ISignal<this, KernelMessage.IIOPubMessage>;
2833
dispose(): void;
2934
/**
3035
* Close all widgets and empty the widget state.

0 commit comments

Comments
 (0)