Skip to content

Commit

Permalink
Merge pull request #32 from statelyai/davidkpiano/context-event-sanit…
Browse files Browse the repository at this point in the history
…ization

Add sanitization
  • Loading branch information
davidkpiano authored Jul 6, 2024
2 parents d3861d4 + 6f7b65a commit 3b5f025
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 6 deletions.
24 changes: 24 additions & 0 deletions .changeset/neat-beans-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
"@statelyai/inspect": minor
---

Added new options `sanitizeContext` and `sanitizeEvent` to the inspector configuration. These options allow users to sanitize sensitive data from the context and events before they are sent to the inspector, and also to remove non-serializable data.

Example usage:

```typescript
const inspector = createInspector({
sanitizeContext: (context) => {
// Remove sensitive data from context
const { password, ...safeContext } = context;
return safeContext;
},
sanitizeEvent: (event) => {
// Remove sensitive data from event
if (event.type === 'SUBMIT_FORM') {
const { creditCardNumber, ...safeEvent } = event;
return safeEvent;
}
return event;
}
});
63 changes: 63 additions & 0 deletions src/createInspector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,69 @@ test('options.serialize', async () => {
});
});

test('Sanitization options', async () => {
const events: StatelyInspectionEvent[] = [];
const testAdapter: Adapter = {
send: (event) => {
events.push(event);
},
start: () => {},
stop: () => {},
};
const inspector = createInspector(testAdapter, {
sanitizeContext: (ctx) => ({
...ctx,
user: 'anonymous',
}),
sanitizeEvent: (ev) => {
if ('user' in ev) {
return { ...ev, user: 'anonymous' };
} else {
return ev;
}
},
});

inspector.actor('test', { context: { user: 'David' } });

expect((events[0] as StatelyActorEvent).snapshot.context).toEqual({
user: 'anonymous',
});

inspector.snapshot('test', { context: { user: 'David' } });

expect((events[1] as StatelyActorEvent).snapshot.context).toEqual({
user: 'anonymous',
});

inspector.event('test', { type: 'updateUser', user: 'David' });

expect((events[2] as StatelyEventEvent).event).toEqual({
type: 'updateUser',
user: 'anonymous',
});

inspector.inspect.next?.({
type: '@xstate.event',
actorRef: {} as any,
event: {
type: 'setUser',
user: 'Another',
},
rootId: '',
sourceRef: undefined,
});

await new Promise<void>((res) => {
setTimeout(res, 10);
});

expect((events[3] as StatelyEventEvent).event).toEqual({
type: 'setUser',
user: 'anonymous',
});
});

test('it safely stringifies objects with circular dependencies', () => {
const events: StatelyInspectionEvent[] = [];
const testAdapter: Adapter = {
Expand Down
63 changes: 57 additions & 6 deletions src/createInspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ import {
} from './types';
import { toEventObject } from './utils';
import { Inspector } from './types';
import { AnyActorRef, InspectionEvent, Snapshot } from 'xstate';
import {
AnyActorRef,
AnyEventObject,
InspectionEvent,
MachineContext,
Snapshot,
} from 'xstate';
import pkg from '../package.json';
import { idleCallback } from './idleCallback';
import safeStringify from 'safe-stable-stringify';
Expand Down Expand Up @@ -40,29 +46,74 @@ export interface InspectorOptions {
* @default true
*/
autoStart?: boolean;
/**
* The maximum number of deferred events to hold in memory until the inspector is active.
* If the number of deferred events exceeds this number, the oldest events will be dropped.
*
* @default 200
*/
maxDeferredEvents?: number;

/**
* Sanitizes events sent to actors. Only the sanitized event will be sent to the inspector.
*/
sanitizeEvent?: (event: AnyEventObject) => AnyEventObject;

/**
* Sanitizes actor snapshot context. Only the sanitized context will be sent to the inspector.
*/
sanitizeContext?: (context: MachineContext) => MachineContext;
}

export const defaultInspectorOptions: Required<InspectorOptions> = {
filter: () => true,
serialize: (event) => event,
autoStart: true,
maxDeferredEvents: 200,
sanitizeEvent: (event) => event,
sanitizeContext: (context) => context,
};

export function createInspector<TAdapter extends Adapter>(
adapter: TAdapter,
options?: InspectorOptions
): Inspector<TAdapter> {
function sendAdapter(event: StatelyInspectionEvent): void {
if (options?.filter && !options.filter(event)) {
function sendAdapter(inspectionEvent: StatelyInspectionEvent): void {
if (options?.filter && !options.filter(inspectionEvent)) {
// Event filtered out
return;
}
const serializedEvent = options?.serialize?.(event) ?? event;
// idleCallback(() => {

const sanitizedEvent: typeof inspectionEvent =
options?.sanitizeContext || options?.sanitizeEvent
? inspectionEvent
: {
...inspectionEvent,
};
if (
options?.sanitizeContext &&
(sanitizedEvent.type === '@xstate.actor' ||
sanitizedEvent.type === '@xstate.snapshot')
) {
sanitizedEvent.snapshot = {
...sanitizedEvent.snapshot,
// @ts-ignore
context: options.sanitizeContext(
// @ts-ignore
sanitizedEvent.snapshot.context
),
};
}
if (
options?.sanitizeEvent &&
(sanitizedEvent.type === '@xstate.event' ||
sanitizedEvent.type === '@xstate.snapshot')
) {
sanitizedEvent.event = options.sanitizeEvent(sanitizedEvent.event);
}
const serializedEvent =
options?.serialize?.(sanitizedEvent) ?? sanitizedEvent;
adapter.send(serializedEvent);
// })
}
const inspector: Inspector<TAdapter> = {
adapter,
Expand Down

0 comments on commit 3b5f025

Please sign in to comment.