Skip to content

Commit

Permalink
✨ (sample): Possibility to export all logs to JSON
Browse files Browse the repository at this point in the history
  • Loading branch information
ofreyssinet-ledger committed Oct 11, 2024
1 parent 2249dd7 commit ea615d7
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 14 deletions.
6 changes: 6 additions & 0 deletions .changeset/light-pears-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@ledgerhq/device-management-kit": patch
"@ledgerhq/device-sdk-sample": patch
---

Add possibility to export logs to JSON
24 changes: 11 additions & 13 deletions apps/sample/src/components/Sidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
"use client";
import React, { useCallback, useEffect, useState } from "react";
import { Box, Flex, Icons, Link, Text } from "@ledgerhq/react-ui";
import { Box, Flex, IconsLegacy, Link, Text } from "@ledgerhq/react-ui";
import { useRouter } from "next/navigation";
import styled, { DefaultTheme } from "styled-components";

import { Device } from "@/components/Device";
import { Menu } from "@/components/Menu";
import { useSdk } from "@/providers/DeviceSdkProvider";
import { useExportLogsCallback, useSdk } from "@/providers/DeviceSdkProvider";
import { useDeviceSessionsContext } from "@/providers/DeviceSessionsProvider";

const Root = styled(Flex).attrs({ py: 8, px: 6 })`
Expand All @@ -29,20 +29,14 @@ const BottomContainer = styled(Flex)`
align-items: center;
`;

const LogsContainer = styled(Flex).attrs({ mb: 6 })`
flex-direction: row;
align-items: center;
`;

const LogsText = styled(Text).attrs({ ml: 3 })``;

const VersionText = styled(Text)`
color: ${({ theme }: { theme: DefaultTheme }) => theme.colors.neutral.c50};
`;

export const Sidebar: React.FC = () => {
const [version, setVersion] = useState("");
const sdk = useSdk();
const exportLogs = useExportLogsCallback();
const {
state: { deviceById, selectedId },
dispatch,
Expand Down Expand Up @@ -105,10 +99,14 @@ export const Sidebar: React.FC = () => {
</MenuContainer>

<BottomContainer>
<LogsContainer>
<Icons.ExternalLink />
<LogsText variant={"paragraph"}>Share logs</LogsText>
</LogsContainer>
<Link
mb={6}
onClick={exportLogs}
size="large"
Icon={IconsLegacy.ExternalLinkMedium}
>
Share logs
</Link>
<VersionText variant={"body"}>
Ledger Device Management Kit version {version}
</VersionText>
Expand Down
12 changes: 11 additions & 1 deletion apps/sample/src/providers/DeviceSdkProvider/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import React from "react";
import React, { useCallback } from "react";
import { createContext, PropsWithChildren, useContext } from "react";
import {
ConsoleLogger,
DeviceSdk,
DeviceSdkBuilder,
WebLogsExporterLogger,
} from "@ledgerhq/device-management-kit";

const webLogsExporterLogger = new WebLogsExporterLogger();

export const sdk = new DeviceSdkBuilder()
.addLogger(new ConsoleLogger())
.addLogger(webLogsExporterLogger)
.build();

const SdkContext = createContext<DeviceSdk>(sdk);
Expand All @@ -19,3 +23,9 @@ export const SdkProvider: React.FC<PropsWithChildren> = ({ children }) => {
export const useSdk = (): DeviceSdk => {
return useContext(SdkContext);
};

export function useExportLogsCallback() {
return useCallback(() => {
webLogsExporterLogger.exportLogsToJSON();
}, []);
}
1 change: 1 addition & 0 deletions packages/core/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export { LedgerDeviceSdkBuilder as DeviceSdkBuilder } from "./DeviceSdkBuilder";
export { DeviceExchangeError, UnknownDeviceExchangeError } from "./Error";
export { LogLevel } from "./logger-subscriber/model/LogLevel";
export { ConsoleLogger } from "./logger-subscriber/service/ConsoleLogger";
export { WebLogsExporterLogger } from "./logger-subscriber/service/WebLogsExporterLogger";
export * from "./types";
export { ConnectedDevice } from "./usb/model/ConnectedDevice";
export { InvalidStatusWordError } from "@api/command/Errors";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { ConnectionType } from "@api/discovery/ConnectionType";
import { deviceModelStubBuilder } from "@internal/device-model/model/DeviceModel.stub";
import { DeviceSession } from "@internal/device-session/model/DeviceSession";
import { ManagerApiService } from "@internal/manager-api/service/ManagerApiService";

import { getJSONStringifyReplacer } from "./WebLogsExporterLogger";

describe("getJSONStringifyReplacer", () => {
it("should return a function that replaces Uint8Array correctly", () => {
const replacer = getJSONStringifyReplacer();
const value = new Uint8Array([1, 2, 3]);
const result = replacer("key", value);
expect(result).toEqual({
hex: "0x010203",
readableHex: "01 02 03",
value: "1,2,3",
});
});

it("should return a function that replaces DeviceSession", () => {
const stubDeviceModel = deviceModelStubBuilder();
const replacer = getJSONStringifyReplacer();

const connectedDevice = {
deviceModel: deviceModelStubBuilder(),
type: "USB" as ConnectionType,
id: "mockedDeviceId",
sendApdu: jest.fn(),
};

const value = new DeviceSession(
{
connectedDevice,
id: "mockedSessionId",
},
jest.fn(),
{} as ManagerApiService,
);
const result = JSON.stringify(value, replacer);
const expected = `{"id":"mockedSessionId","connectedDevice":{"deviceModel":${JSON.stringify(
stubDeviceModel,
)},"type":"USB","id":"mockedDeviceId"}}`;
expect(result).toEqual(expected);
});

it("should return a function that replaces circular references", () => {
interface CircularObject {
name: string;
self?: CircularObject;
}

const obj: CircularObject = { name: "Alice" };
obj.self = obj;

const expected = '{"name":"Alice","self":"[Circular]"}';
const result = JSON.stringify(obj, getJSONStringifyReplacer());
expect(result).toEqual(expected);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { LogLevel } from "@api/logger-subscriber/model/LogLevel";
import { LogSubscriberOptions } from "@api/types";
import { DeviceSession } from "@internal/device-session/model/DeviceSession";

import { LoggerSubscriberService } from "./LoggerSubscriberService";

/**
* This function is used to format the logs to JSON format,
* remove circular dependencies and do some extra formatting.
* */
export function getJSONStringifyReplacer(): (
key: string,
value: unknown,
) => unknown {
const ancestors: unknown[] = [];
return function (_: string, value: unknown): unknown {
// format Uint8Array values to more readable format
if (value instanceof Uint8Array) {
const bytesHex = Array.from(value).map((x) =>
x.toString(16).padStart(2, "0"),
);
return {
hex: "0x" + bytesHex.join(""),
readableHex: bytesHex.join(" "),
value: value.toString(),
};
}

// format DeviceSession values to avoid huge object in logs
if (value instanceof DeviceSession) {
const {
connectedDevice: { deviceModel, type, id },
} = value;
return {
id: value.id,
connectedDevice: {
deviceModel,
type,
id,
},
};
}

// format circular references to "[Circular]"
// Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value#circular_references
if (typeof value !== "object" || value === null) {
return value;
}
// `this` is the object that value is contained in,
// i.e., its direct parent.
// @ts-expect-error cf. comment above
while (ancestors.length > 0 && ancestors.at(-1) !== (this as unknown)) {
ancestors.pop();
}
if (ancestors.includes(value)) {
return "[Circular]";
}
ancestors.push(value);
return value;
};
}

export class WebLogsExporterLogger implements LoggerSubscriberService {
private logs: Array<
[level: LogLevel, message: string, options: LogSubscriberOptions]
> = [];

log(level: LogLevel, message: string, options: LogSubscriberOptions): void {
this.logs.push([level, message, options]);
}

private formatLogsToJSON(): string {
const remappedLogs = this.logs.map(([level, message, options]) => {
const { timestamp, ...restOptions } = options;
return {
level: LogLevel[level],
message,
options: {
...restOptions,
date: new Date(options.timestamp),
},
};
});

return JSON.stringify(remappedLogs, getJSONStringifyReplacer(), 2);
}

/**
* Export logs to JSON file.
*/
public exportLogsToJSON(): void {
const logs = this.formatLogsToJSON();
const blob = new Blob([logs], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `ledger-device-management-kit-logs-${new Date().toISOString()}.json`;
a.click();
}
}

0 comments on commit ea615d7

Please sign in to comment.