Fix Network serialization

Summary:
Changelog: [Network] Fix import / export of binary data

Introduced proper serialization of binary data when creating a Flipper export.
Implements / solves https://github.com/facebook/flipper/issues/2308

Reviewed By: nikoant

Differential Revision: D28441021

fbshipit-source-id: 90b524bf2a5d85e373073b50a3ccf2bb29628ee0
This commit is contained in:
Michel Weststrate
2021-05-17 03:15:46 -07:00
committed by Facebook GitHub Bot
parent 5242a81e94
commit b947a65c51
4 changed files with 201 additions and 16 deletions

View File

@@ -11,7 +11,7 @@ import {TestUtils} from 'flipper-plugin';
import * as NetworkPlugin from '../index'; import * as NetworkPlugin from '../index';
test('Can handle custom headers', async () => { test('Can handle custom headers', async () => {
const {instance, sendEvent, act, renderer, exportState} = const {instance, sendEvent, act, renderer, exportStateAsync} =
TestUtils.renderPlugin(NetworkPlugin); TestUtils.renderPlugin(NetworkPlugin);
act(() => { act(() => {
@@ -122,7 +122,7 @@ test('Can handle custom headers', async () => {
// after import, columns should be visible and restored // after import, columns should be visible and restored
{ {
const snapshot = exportState(); const snapshot = await exportStateAsync();
// Note: snapshot is set in the previous test // Note: snapshot is set in the previous test
const {instance: instance2, renderer: renderer2} = TestUtils.renderPlugin( const {instance: instance2, renderer: renderer2} = TestUtils.renderPlugin(
NetworkPlugin, NetworkPlugin,

View File

@@ -13,6 +13,8 @@ import {decodeBody} from '../utils';
import {ResponseInfo} from '../types'; import {ResponseInfo} from '../types';
import {promisify} from 'util'; import {promisify} from 'util';
import {readFileSync} from 'fs'; import {readFileSync} from 'fs';
import {TestUtils} from 'flipper-plugin';
import * as NetworkPlugin from '../index';
async function createMockResponse( async function createMockResponse(
input: string, input: string,
@@ -136,3 +138,122 @@ describe('network data encoding', () => {
expect(bodyAsBuffer(response)).toEqual(tinyLogoExpected); expect(bodyAsBuffer(response)).toEqual(tinyLogoExpected);
}); });
}); });
test('binary data gets serialized correctly', async () => {
const tinyLogoExpected = readFileSync(
path.join(__dirname, 'fixtures', 'tiny_logo.png'),
);
const tinyLogoData = readFileSync(
path.join(__dirname, 'fixtures', 'tiny_logo.base64.txt'),
'utf-8',
);
const donatingExpected = readFileSync(
path.join(__dirname, 'fixtures', 'donating.md'),
'utf-8',
);
const donatingData = readFileSync(
path.join(__dirname, 'fixtures', 'donating.md.utf8.gzip.ios.txt'),
'utf-8',
);
const {instance, sendEvent, exportStateAsync} =
TestUtils.startPlugin(NetworkPlugin);
sendEvent('newRequest', {
id: '0',
timestamp: 0,
data: donatingData,
headers: [
{
key: 'Content-Type',
value: 'text/plain',
},
],
method: 'post',
url: 'http://www.fbflipper.com',
});
const response = await createMockResponse(
'tiny_logo.android.txt',
'image/png',
);
sendEvent('newResponse', response);
expect(instance.requests.getById('0')).toMatchObject({
requestHeaders: [
{
key: 'Content-Type',
value: 'text/plain',
},
],
requestData: donatingExpected,
responseHeaders: [
{
key: 'Content-Type',
value: 'image/png',
},
],
responseData: new Uint8Array(tinyLogoExpected),
});
const snapshot = await exportStateAsync();
expect(snapshot).toMatchObject({
isMockResponseSupported: true,
selectedId: undefined,
requests2: [
{
domain: 'www.fbflipper.com/',
duration: 0,
id: '0',
insights: undefined,
method: 'post',
reason: 'dunno',
requestHeaders: [
{
key: 'Content-Type',
value: 'text/plain',
},
],
requestData: donatingExpected, // not encoded
responseData: [tinyLogoData.trim()], // wrapped represents base64
responseHeaders: [
{
key: 'Content-Type',
value: 'image/png',
},
],
responseIsMock: false,
responseLength: 24838,
status: 200,
url: 'http://www.fbflipper.com',
},
],
});
const {instance: instance2} = TestUtils.startPlugin(NetworkPlugin, {
initialState: snapshot,
});
expect(instance2.requests.getById('0')).toMatchObject({
domain: 'www.fbflipper.com/',
duration: 0,
id: '0',
insights: undefined,
method: 'post',
reason: 'dunno',
requestHeaders: [
{
key: 'Content-Type',
value: 'text/plain',
},
],
requestData: donatingExpected,
responseData: new Uint8Array(tinyLogoExpected),
responseHeaders: [
{
key: 'Content-Type',
value: 'image/png',
},
],
responseIsMock: false,
responseLength: 24838,
status: 200,
url: 'http://www.fbflipper.com',
});
});

View File

@@ -42,6 +42,7 @@ import {
ResponseFollowupChunk, ResponseFollowupChunk,
AddProtobufEvent, AddProtobufEvent,
PartialResponses, PartialResponses,
SerializedRequest,
} from './types'; } from './types';
import {ProtobufDefinitionsRepository} from './ProtobufDefinitionsRepository'; import {ProtobufDefinitionsRepository} from './ProtobufDefinitionsRepository';
import { import {
@@ -68,6 +69,7 @@ import {
createNetworkManager, createNetworkManager,
computeMockRoutes, computeMockRoutes,
} from './request-mocking/NetworkRouteManager'; } from './request-mocking/NetworkRouteManager';
import {Base64} from 'js-base64';
const LOCALSTORAGE_MOCK_ROUTE_LIST_KEY = '__NETWORK_CACHED_MOCK_ROUTE_LIST'; const LOCALSTORAGE_MOCK_ROUTE_LIST_KEY = '__NETWORK_CACHED_MOCK_ROUTE_LIST';
const LOCALSTORAGE_RESPONSE_BODY_FORMAT_KEY = const LOCALSTORAGE_RESPONSE_BODY_FORMAT_KEY =
@@ -94,6 +96,13 @@ type CustomColumnConfig = {
type: 'response' | 'request'; type: 'response' | 'request';
}; };
type StateExport = {
requests2: SerializedRequest[];
isMockResponseSupported: boolean;
selectedId: string | undefined;
customColumns: CustomColumnConfig[];
};
export function plugin(client: PluginClient<Events, Methods>) { export function plugin(client: PluginClient<Events, Methods>) {
const networkRouteManager = createState<NetworkRouteManager>( const networkRouteManager = createState<NetworkRouteManager>(
nullNetworkRouteManager, nullNetworkRouteManager,
@@ -101,30 +110,20 @@ export function plugin(client: PluginClient<Events, Methods>) {
const routes = createState<{[id: string]: Route}>({}); const routes = createState<{[id: string]: Route}>({});
const nextRouteId = createState<number>(0); const nextRouteId = createState<number>(0);
const isMockResponseSupported = createState<boolean>(false, { const isMockResponseSupported = createState<boolean>(false);
persist: 'isMockResponseSupported',
});
const showMockResponseDialog = createState<boolean>(false); const showMockResponseDialog = createState<boolean>(false);
const detailBodyFormat = createState<string>( const detailBodyFormat = createState<string>(
localStorage.getItem(LOCALSTORAGE_RESPONSE_BODY_FORMAT_KEY) || 'parsed', localStorage.getItem(LOCALSTORAGE_RESPONSE_BODY_FORMAT_KEY) || 'parsed',
); );
const requests = createDataSource<Request, 'id'>([], { const requests = createDataSource<Request, 'id'>([], {
key: 'id', key: 'id',
persist: 'requests2',
});
const selectedId = createState<string | undefined>(undefined, {
persist: 'selectedId',
}); });
const selectedId = createState<string | undefined>(undefined);
const tableManagerRef = createRef<undefined | DataTableManager<Request>>(); const tableManagerRef = createRef<undefined | DataTableManager<Request>>();
const partialResponses = createState<PartialResponses>( const partialResponses = createState<PartialResponses>({});
{},
{persist: 'partialResponses'},
);
const customColumns = createState<CustomColumnConfig[]>([], { const customColumns = createState<CustomColumnConfig[]>([]); // Store in local storage as well: T69989583
persist: 'customColumns', // Store in local storage as well: T69989583
});
const columns = createState<DataTableColumn<Request>[]>(baseColumns); // not persistable const columns = createState<DataTableColumn<Request>[]>(baseColumns); // not persistable
client.onDeepLink((payload: unknown) => { client.onDeepLink((payload: unknown) => {
@@ -337,6 +336,61 @@ export function plugin(client: PluginClient<Events, Methods>) {
customColumns.get().forEach(addDataTableColumnConfig); customColumns.get().forEach(addDataTableColumnConfig);
}); });
client.onExport<StateExport>(async (idler, onStatusMessage) => {
const serializedRequests: SerializedRequest[] = [];
for (let i = 0; i < requests.size; i++) {
const request = requests.get(i);
serializedRequests.push({
...request,
requestTime: request.requestTime.getTime(),
responseTime: request.responseTime?.getTime(),
requestData:
request.requestData instanceof Uint8Array
? [Base64.fromUint8Array(request.requestData)]
: request.requestData,
responseData:
request.responseData instanceof Uint8Array
? [Base64.fromUint8Array(request.responseData)]
: request.responseData,
});
if (idler.isCancelled()) {
return;
}
if (idler.shouldIdle()) {
onStatusMessage(`Serializing request ${i + 1}/${requests.size}`);
await idler.idle();
}
}
return {
isMockResponseSupported: isMockResponseSupported.get(),
selectedId: selectedId.get(),
requests2: serializedRequests,
customColumns: customColumns.get(),
};
});
client.onImport<StateExport>((data) => {
selectedId.set(data.selectedId);
isMockResponseSupported.set(data.isMockResponseSupported);
customColumns.set(data.customColumns);
data.requests2.forEach((request) => {
requests.append({
...request,
requestTime: new Date(request.requestTime),
responseTime:
request.responseTime != null
? new Date(request.responseTime)
: undefined,
requestData: Array.isArray(request.requestData)
? Base64.toUint8Array(request.requestData[0])
: request.requestData,
responseData: Array.isArray(request.responseData)
? Base64.toUint8Array(request.responseData[0])
: request.responseData,
});
});
});
return { return {
columns, columns,
routes, routes,

View File

@@ -35,6 +35,16 @@ export interface Request {
export type Requests = DataSource<Request, 'id', string>; export type Requests = DataSource<Request, 'id', string>;
export type SerializedRequest = Omit<
Request,
'requestTime' | 'responseTime' | 'requestData' | 'responseData'
> & {
requestTime: number;
requestData?: string | [string]; // wrapped in Array represents base64 encoded
responseTime?: number;
responseData?: string | [string]; // wrapped in Array represents base64 encoded
};
export type RequestInfo = { export type RequestInfo = {
id: RequestId; id: RequestId;
timestamp: number; timestamp: number;