From 46e0abecdf4a5dc188619420ecd3938abc909f2a Mon Sep 17 00:00:00 2001 From: Timur Valiev Date: Wed, 14 Aug 2019 03:26:04 -0700 Subject: [PATCH] api + plugins examples Summary: Initial commit for Flipper API for JS apps: 1) Plugin, Connection, Client 2) a bridge to work with WebView proxy app 3) examples of ported plugins: AnalyticsLogging and Fury Reviewed By: danielbuechele Differential Revision: D16753405 fbshipit-source-id: cdd4b863db236b2043f09833aed130035f6738de --- src/utils/js-client/api.js | 113 ++++++++++++++++++ src/utils/js-client/example.js | 38 ++++++ src/utils/js-client/package.json | 11 ++ .../js-client/plugins/analyticsLogging.js | 30 +++++ src/utils/js-client/plugins/fury.js | 104 ++++++++++++++++ src/utils/js-client/webviewImpl.js | 61 ++++++++++ src/utils/js-client/yarn.lock | 4 + 7 files changed, 361 insertions(+) create mode 100644 src/utils/js-client/api.js create mode 100644 src/utils/js-client/example.js create mode 100644 src/utils/js-client/package.json create mode 100644 src/utils/js-client/plugins/analyticsLogging.js create mode 100644 src/utils/js-client/plugins/fury.js create mode 100644 src/utils/js-client/webviewImpl.js create mode 100644 src/utils/js-client/yarn.lock diff --git a/src/utils/js-client/api.js b/src/utils/js-client/api.js new file mode 100644 index 000000000..adae3294b --- /dev/null +++ b/src/utils/js-client/api.js @@ -0,0 +1,113 @@ +/** + * Copyright 2018-present Facebook. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * @format + */ + +export type FlipperPluginID = string; + +export type FlipperMethodID = string; + +export class FlipperBridge { + registerPlugins: (plugins: Array) => void; + + start: () => void; + + stop: () => void; + + sendData: ( + plugin: FlipperPluginID, + method: FlipperMethodID, + data: any, + ) => void; + + subscribe: ( + plugin: FlipperPluginID, + method: FlipperMethodID, + handler: (any) => void, + ) => void; + + isAvailable: () => boolean; +} + +export class FlipperResponder { + pluginId: FlipperPluginID; + methodId: FlipperMethodID; + _bridge: FlipperBridge; + + constructor( + pluginId: FlipperPluginID, + methodId: FlipperMethodID, + bridge: FlipperBridge, + ) { + this.pluginId = pluginId; + this.methodId = methodId; + this._bridge = bridge; + } + + success(response: any) {} + + error(response: any) {} +} + +export type FlipperReceiver = ( + params: T, + responder: FlipperResponder, +) => void; + +export class FlipperConnection { + pluginId: FlipperPluginID; + _bridge: FlipperBridge; + + constructor(pluginId: FlipperPluginID, bridge: FlipperBridge) { + this.pluginId = pluginId; + this._bridge = bridge; + } + + send(method: FlipperMethodID, data: any) { + this._bridge.sendData(this.pluginId, method, data); + } + + receive(method: FlipperMethodID, receiver: FlipperReceiver<*>) { + this._bridge.subscribe(this.pluginId, method, data => { + receiver(data, new FlipperResponder(this.pluginId, method, this._bridge)); + }); + } +} + +export class FlipperPlugin { + id: FlipperPluginID; + _connection: ?FlipperConnection; + + onConnect(connection: FlipperConnection) { + this._connection = connection; + } +} + +export class FlipperClient { + _bridge: FlipperBridge; + plugins: Map = new Map(); + + constructor(bridge: FlipperBridge) { + this._bridge = bridge; + } + + addPlugin(plugin: FlipperPlugin) { + plugin.onConnect(new FlipperConnection(plugin.id, this._bridge)); + this.plugins.set(plugin.id, plugin); + } + + getPlugin(id: FlipperPluginID): ?FlipperPlugin { + return this.plugins.get(id); + } + + start() { + this._bridge.registerPlugins([...this.plugins.keys()]); + this._bridge.start(); + } + + stop() { + this._bridge.stop(); + } +} diff --git a/src/utils/js-client/example.js b/src/utils/js-client/example.js new file mode 100644 index 000000000..338053926 --- /dev/null +++ b/src/utils/js-client/example.js @@ -0,0 +1,38 @@ +/** + * Copyright 2018-present Facebook. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * @format + */ + +import {FlipperClient} from './api'; +import {newWebviewClient} from './webviewImpl'; +import {AnalyticsLoggingFlipperPlugin} from './plugins/analyticsLogging'; +import {FuryPlugin} from './plugins/fury'; + +class FlipperManager { + flipperClient: FlipperClient; + analyticsPlugin: AnalyticsLoggingFlipperPlugin; + furyPlugin: FuryPlugin; + + constructor() { + this.flipperClient = newWebviewClient(); + this.analyticsPlugin = new AnalyticsLoggingFlipperPlugin(); + this.furyPlugin = new FuryPlugin(); + this.flipperClient.addPlugin(this.analyticsPlugin); + this.flipperClient.addPlugin(this.furyPlugin); + this.flipperClient.start(); + } +} + +let flipperManager: ?FlipperManager; + +export function init() { + if (!flipperManager) { + flipperManager = new FlipperManager(); + } +} + +export function flipper(): ?FlipperManager { + return flipperManager; +} diff --git a/src/utils/js-client/package.json b/src/utils/js-client/package.json new file mode 100644 index 000000000..ef3dd154e --- /dev/null +++ b/src/utils/js-client/package.json @@ -0,0 +1,11 @@ +{ + "name": "flipper-js-client", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "title": "Flipper JS Client", + "icon": "apps", + "bugs": { + "email": "timurvaliev@fb.com" + } + } diff --git a/src/utils/js-client/plugins/analyticsLogging.js b/src/utils/js-client/plugins/analyticsLogging.js new file mode 100644 index 000000000..9d1c59421 --- /dev/null +++ b/src/utils/js-client/plugins/analyticsLogging.js @@ -0,0 +1,30 @@ +/** + * Copyright 2018-present Facebook. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * @format + */ + +import {FlipperPlugin} from '../api'; + +import type {FlipperPluginID} from '../api'; + +type EventId = string; + +export type AnalyticsEvent = { + id: EventId, + module: string, + name: string, + time: number, + filter: ?string, + highpri: ?boolean, + extras: any, +}; + +export class AnalyticsLoggingFlipperPlugin extends FlipperPlugin { + id: FlipperPluginID = 'AnalyticsLogging'; + + sendEvent(event: AnalyticsEvent) { + this._connection && this._connection.send('reportEvent', event); + } +} diff --git a/src/utils/js-client/plugins/fury.js b/src/utils/js-client/plugins/fury.js new file mode 100644 index 000000000..b5a6a5f5d --- /dev/null +++ b/src/utils/js-client/plugins/fury.js @@ -0,0 +1,104 @@ +/** + * Copyright 2018-present Facebook. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * @format + * @flow + */ + +import {FlipperPlugin, FlipperResponder, FlipperConnection} from '../api'; + +import type {FlipperPluginID} from '../api'; + +type EventId = string; + +type EventType = number; +type ThreadID = number; +type SeqID = number; + +type StackTraceElement = { + className: string, + methodName: string, + fileName: string, + lineNumber: number, +}; + +type FuryTaskEvent = { + id: EventId, + time: number, + eventType: EventType, + callStack: StackTraceElement[], + extras: any, + tag: string, + parentTid: ThreadID, + currentTid: ThreadID, + parentSeqId: SeqID, + currentSeqId: SeqID, + isDirect: boolean, + isPoint: boolean, + isOnActivated: boolean, +}; + +export type ReqContext = { + tag: string, + parentSeqId: SeqID, + currentSeqId: SeqID, + isDirect: boolean, + isPoint: boolean, +}; + +const structuredTraceFun = (error, structuredStackTrace) => { + return structuredStackTrace; +}; + +function getStackTrace(): CallSite[] { + const oldPrep = Error.prepareStackTrace; + Error.prepareStackTrace = structuredTraceFun; + const error = {}; + Error.captureStackTrace(error, getStackTrace); + const stack = error.stack; + Error.prepareStackTrace = oldPrep; + return stack; +} + +export class FuryPlugin extends FlipperPlugin { + id: FlipperPluginID = 'Fury'; + + onConnect(connection: FlipperConnection) { + super.onConnect(connection); + connection.receive( + 'toggleRecording', + (data: any, responder: FlipperResponder) => { + window.console.log(data); + }, + ); + } + + processEvent(reqContext: ReqContext, isOnActivated: boolean) { + const stack = getStackTrace(); + const eventType = reqContext.isPoint ? 2 : isOnActivated ? 0 : 1; + const event: FuryTaskEvent = { + id: reqContext.currentSeqId + '/' + eventType, + time: new Date().getTime(), + eventType: eventType, + callStack: stack.map(frame => { + return { + className: frame.getTypeName() || '', + methodName: frame.getFunctionName() || '', + fileName: frame.getFileName() || '', + lineNumber: frame.getLineNumber() || 0, + }; + }), + extras: {}, + tag: reqContext.tag, + parentTid: -1, + currentTid: -1, + parentSeqId: reqContext.parentSeqId, + currentSeqId: reqContext.currentSeqId, + isDirect: reqContext.isDirect, + isPoint: reqContext.isPoint, + isOnActivated: isOnActivated, + }; + this._connection && this._connection.send('reportEvent', event); + } +} diff --git a/src/utils/js-client/webviewImpl.js b/src/utils/js-client/webviewImpl.js new file mode 100644 index 000000000..aed768b2c --- /dev/null +++ b/src/utils/js-client/webviewImpl.js @@ -0,0 +1,61 @@ +/** + * Copyright 2018-present Facebook. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * @format + */ + +import {FlipperClient, FlipperBridge} from './api'; + +import type {FlipperPluginID, FlipperMethodID} from './api'; + +class FlipperWebviewBridgeImpl extends FlipperBridge { + _subscriptions: Map void> = new Map(); + + registerPlugins = (plugins: Array) => { + window.FlipperWebviewBridge && + window.FlipperWebviewBridge.registerPlugins(plugins); + }; + + start = () => { + window.FlipperWebviewBridge && window.FlipperWebviewBridge.start(); + }; + + stop = () => { + window.FlipperWebviewBridge && window.FlipperWebviewBridge.stop(); + }; + + sendData = (plugin: FlipperPluginID, method: FlipperMethodID, data: any) => { + window.FlipperWebviewBridge && + window.FlipperWebviewBridge.sendFlipperObject( + plugin, + method, + JSON.stringify(data), + ); + }; + + subscribe = ( + plugin: FlipperPluginID, + method: FlipperMethodID, + handler: any => void, + ) => { + this._subscriptions.set(plugin + method, handler); + window.FlipperWebviewBridge && + window.FlipperWebviewBridge.subscribe(plugin, method); + }; + + isAvailable = () => { + return window.FlipperWebviewBridge != null; + }; + + handleMessage(plugin: FlipperPluginID, method: FlipperMethodID, data: any) { + const handler: ?(any) => void = this._subscriptions.get(plugin + method); + handler && handler(data); + } +} + +export function newWebviewClient(): FlipperClient { + const bridge = new FlipperWebviewBridgeImpl(); + window.FlipperBridgeClientSide = bridge; + return new FlipperClient(bridge); +} diff --git a/src/utils/js-client/yarn.lock b/src/utils/js-client/yarn.lock new file mode 100644 index 000000000..fb57ccd13 --- /dev/null +++ b/src/utils/js-client/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + +