@principal-ai/storybook-addon-otel 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,220 @@
1
+ # @principal-ai/storybook-addon-otel
2
+
3
+ Capture and visualize OpenTelemetry data from Storybook story interactions.
4
+
5
+ ## Features
6
+
7
+ - 📊 **Timeline View**: See all spans and events in chronological order
8
+ - 🎨 **Canvas View**: Visualize telemetry flow on an interactive canvas (coming soon)
9
+ - 📤 **Export**: Download telemetry as JSON for analysis
10
+ - 🔄 **Real-time**: Updates as you interact with stories
11
+ - 🎯 **Automatic**: Wraps stories with telemetry collection
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @principal-ai/storybook-addon-otel --save-dev
17
+ # or
18
+ yarn add @principal-ai/storybook-addon-otel --dev
19
+ # or
20
+ pnpm add @principal-ai/storybook-addon-otel --save-dev
21
+ ```
22
+
23
+ ## Setup
24
+
25
+ ### 1. Register the Addon
26
+
27
+ Add the addon to your `.storybook/main.js` or `.storybook/main.ts`:
28
+
29
+ ```typescript
30
+ import type { StorybookConfig } from '@storybook/react-vite';
31
+
32
+ const config: StorybookConfig = {
33
+ addons: [
34
+ // ... other addons
35
+ '@principal-ai/storybook-addon-otel',
36
+ ],
37
+ };
38
+
39
+ export default config;
40
+ ```
41
+
42
+ ### 2. Start Storybook
43
+
44
+ ```bash
45
+ npm run storybook
46
+ ```
47
+
48
+ ### 3. View Telemetry
49
+
50
+ 1. Open any story
51
+ 2. Look for the **"OTEL Telemetry"** panel at the bottom (next to Actions, Controls, etc.)
52
+ 3. Interact with your story to see telemetry captured in real-time
53
+
54
+ ## Usage
55
+
56
+ ### Automatic Telemetry
57
+
58
+ By default, the addon automatically captures:
59
+ - Story mount/unmount events
60
+ - Story metadata (title, name, id)
61
+
62
+ ### Manual Instrumentation
63
+
64
+ For custom telemetry, import and use the `TelemetryCollector`:
65
+
66
+ ```typescript
67
+ import { TelemetryCollector } from '@principal-ai/storybook-addon-otel/preview';
68
+
69
+ export const MyStory = () => {
70
+ const handleClick = () => {
71
+ // Create a span
72
+ const span = TelemetryCollector.startSpan('button-click');
73
+
74
+ // Add an event
75
+ TelemetryCollector.addEvent(span, 'clicked', {
76
+ 'button.id': 'submit',
77
+ 'button.text': 'Submit',
78
+ });
79
+
80
+ // End the span
81
+ TelemetryCollector.endSpan(span);
82
+ };
83
+
84
+ return <button onClick={handleClick}>Click me</button>;
85
+ };
86
+ ```
87
+
88
+ ### Instrumenting Event Emitters
89
+
90
+ ```typescript
91
+ import { TelemetryCollector } from '@principal-ai/storybook-addon-otel/preview';
92
+
93
+ // Wrap your event emitter
94
+ const originalEmit = events.emit.bind(events);
95
+ events.emit = (event) => {
96
+ const span = TelemetryCollector.startSpan(event.type);
97
+ TelemetryCollector.addEvent(span, event.type, event.payload);
98
+ TelemetryCollector.endSpan(span);
99
+ return originalEmit(event);
100
+ };
101
+ ```
102
+
103
+ ### Disabling Telemetry for Specific Stories
104
+
105
+ ```typescript
106
+ export default {
107
+ title: 'My Story',
108
+ parameters: {
109
+ otelTelemetry: {
110
+ enabled: false, // Disable for this story
111
+ },
112
+ },
113
+ };
114
+ ```
115
+
116
+ ## API Reference
117
+
118
+ ### `TelemetryCollector`
119
+
120
+ #### `startSpan(name, attributes?, parentId?)`
121
+ Create a new telemetry span.
122
+
123
+ **Parameters:**
124
+ - `name` (string): Name of the span
125
+ - `attributes` (object, optional): Key-value pairs of span attributes
126
+ - `parentId` (string, optional): ID of parent span for hierarchical spans
127
+
128
+ **Returns:** TelemetrySpan object
129
+
130
+ #### `addEvent(span, eventName, attributes?)`
131
+ Add an event to a span.
132
+
133
+ **Parameters:**
134
+ - `span` (TelemetrySpan): The span to add the event to
135
+ - `eventName` (string): Name of the event
136
+ - `attributes` (object, optional): Key-value pairs of event attributes
137
+
138
+ #### `endSpan(span)`
139
+ End a span and calculate its duration.
140
+
141
+ **Parameters:**
142
+ - `span` (TelemetrySpan): The span to end
143
+
144
+ #### `getSpans()`
145
+ Get all collected spans.
146
+
147
+ **Returns:** Array of TelemetrySpan objects
148
+
149
+ #### `clear()`
150
+ Clear all collected telemetry.
151
+
152
+ ## Panel Features
153
+
154
+ ### Timeline View
155
+ - Shows all spans chronologically
156
+ - Displays span names, durations, and status
157
+ - Shows attributes and events for each span
158
+ - Color-coded by status (OK = green, ERROR = red)
159
+
160
+ ### Canvas View (Coming Soon)
161
+ - Visual representation of execution flow
162
+ - Interactive graph showing span relationships
163
+ - Integrates with Principal View GraphRenderer
164
+
165
+ ### Export
166
+ - Download telemetry as JSON
167
+ - Compatible with OpenTelemetry trace format
168
+ - Can be imported into analysis tools
169
+
170
+ ## Development
171
+
172
+ ### Building
173
+
174
+ ```bash
175
+ npm run build
176
+ ```
177
+
178
+ ### Local Development
179
+
180
+ Link the package locally:
181
+
182
+ ```bash
183
+ cd packages/storybook-addon-otel
184
+ npm link
185
+
186
+ # In your project
187
+ npm link @principal-ai/storybook-addon-otel
188
+ ```
189
+
190
+ ## Examples
191
+
192
+ See the `examples/` directory for complete examples:
193
+ - Basic telemetry capture
194
+ - Event emitter instrumentation
195
+ - Complex lifecycle tracking
196
+ - Integration with canvas schemas
197
+
198
+ ## Integration with Principal View
199
+
200
+ This addon is designed to work with the Principal View ecosystem:
201
+
202
+ - Define event schemas in `.otel.canvas` files
203
+ - Validate captured events against schemas
204
+ - Visualize execution flows on canvas
205
+ - Export telemetry for offline analysis
206
+
207
+ See the [Principal View documentation](https://github.com/principal-ai/principal-view-core-library) for more information.
208
+
209
+ ## License
210
+
211
+ MIT
212
+
213
+ ## Contributing
214
+
215
+ Contributions are welcome! Please see the [contributing guide](../../CONTRIBUTING.md).
216
+
217
+ ## Support
218
+
219
+ - [GitHub Issues](https://github.com/principal-ai/principal-view-core-library/issues)
220
+ - [Documentation](https://github.com/principal-ai/principal-view-core-library/tree/main/packages/storybook-addon-otel)
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Telemetry Collector
3
+ *
4
+ * Captures spans and events during story interactions
5
+ */
6
+ export interface SpanEvent {
7
+ time: number;
8
+ name: string;
9
+ attributes: Record<string, string | number | boolean>;
10
+ }
11
+ export interface TelemetrySpan {
12
+ id: string;
13
+ name: string;
14
+ parentId?: string;
15
+ startTime: number;
16
+ endTime?: number;
17
+ duration?: number;
18
+ attributes: Record<string, string | number | boolean>;
19
+ events: SpanEvent[];
20
+ status: 'OK' | 'ERROR';
21
+ errorMessage?: string;
22
+ }
23
+ declare class TelemetryCollectorImpl {
24
+ private spans;
25
+ private spanIdCounter;
26
+ private activeSpans;
27
+ private channel;
28
+ constructor();
29
+ startSpan(name: string, attributes?: Record<string, string | number | boolean>, parentId?: string): TelemetrySpan;
30
+ addEvent(span: TelemetrySpan, eventName: string, attributes?: Record<string, string | number | boolean>): void;
31
+ endSpan(span: TelemetrySpan): void;
32
+ markError(span: TelemetrySpan, error: Error | string): void;
33
+ getSpans(): TelemetrySpan[];
34
+ clear(): void;
35
+ private emit;
36
+ exportJson(): string;
37
+ }
38
+ export declare const TelemetryCollector: TelemetryCollectorImpl;
39
+ export {};
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ /**
3
+ * Telemetry Collector
4
+ *
5
+ * Captures spans and events during story interactions
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.TelemetryCollector = void 0;
9
+ const manager_api_1 = require("@storybook/manager-api");
10
+ const constants_1 = require("./constants");
11
+ class TelemetryCollectorImpl {
12
+ constructor() {
13
+ this.spans = [];
14
+ this.spanIdCounter = 0;
15
+ this.activeSpans = new Map();
16
+ // Get communication channel with manager
17
+ if (typeof manager_api_1.addons !== 'undefined') {
18
+ this.channel = manager_api_1.addons.getChannel();
19
+ }
20
+ }
21
+ startSpan(name, attributes, parentId) {
22
+ const span = {
23
+ id: `span-${++this.spanIdCounter}`,
24
+ name,
25
+ parentId,
26
+ startTime: Date.now(),
27
+ attributes: {
28
+ 'component.name': name,
29
+ ...attributes,
30
+ },
31
+ events: [],
32
+ status: 'OK',
33
+ };
34
+ this.spans.push(span);
35
+ this.activeSpans.set(span.id, span);
36
+ console.log('[TelemetryCollector] Started span:', name, 'Total spans:', this.spans.length);
37
+ this.emit();
38
+ return span;
39
+ }
40
+ addEvent(span, eventName, attributes) {
41
+ span.events.push({
42
+ time: Date.now(),
43
+ name: eventName,
44
+ attributes: attributes || {},
45
+ });
46
+ this.emit();
47
+ }
48
+ endSpan(span) {
49
+ span.endTime = Date.now();
50
+ span.duration = span.endTime - span.startTime;
51
+ this.activeSpans.delete(span.id);
52
+ this.emit();
53
+ }
54
+ markError(span, error) {
55
+ span.status = 'ERROR';
56
+ span.errorMessage = typeof error === 'string' ? error : error.message;
57
+ span.attributes['error'] = true;
58
+ span.attributes['error.message'] = span.errorMessage;
59
+ this.emit();
60
+ }
61
+ getSpans() {
62
+ return [...this.spans];
63
+ }
64
+ clear() {
65
+ this.spans = [];
66
+ this.spanIdCounter = 0;
67
+ this.activeSpans.clear();
68
+ this.emit();
69
+ }
70
+ emit() {
71
+ console.log('[TelemetryCollector] Emitting update, spans:', this.spans.length, 'has channel:', !!this.channel);
72
+ if (this.channel) {
73
+ this.channel.emit(constants_1.EVENTS.TELEMETRY_UPDATE, this.getSpans());
74
+ }
75
+ }
76
+ exportJson() {
77
+ // End any active spans
78
+ this.activeSpans.forEach(span => this.endSpan(span));
79
+ return JSON.stringify(this.spans, null, 2);
80
+ }
81
+ }
82
+ // Singleton instance
83
+ exports.TelemetryCollector = new TelemetryCollectorImpl();
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Telemetry Panel Component
3
+ *
4
+ * Displays captured telemetry in Storybook panel
5
+ */
6
+ import React from 'react';
7
+ interface PanelProps {
8
+ active: boolean;
9
+ }
10
+ export declare const TelemetryPanel: React.FC<PanelProps>;
11
+ export {};
@@ -0,0 +1,235 @@
1
+ "use strict";
2
+ /**
3
+ * Telemetry Panel Component
4
+ *
5
+ * Displays captured telemetry in Storybook panel
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.TelemetryPanel = void 0;
42
+ const react_1 = __importStar(require("react"));
43
+ const manager_api_1 = require("@storybook/manager-api");
44
+ const components_1 = require("@storybook/components");
45
+ const theming_1 = require("@storybook/theming");
46
+ const constants_1 = require("../constants");
47
+ const PanelContainer = theming_1.styled.div `
48
+ padding: 16px;
49
+ font-family: monospace;
50
+ font-size: 12px;
51
+ height: 100%;
52
+ overflow: auto;
53
+ background: #1a1a1a;
54
+ color: #e6e6e6;
55
+ `;
56
+ const Header = theming_1.styled.div `
57
+ display: flex;
58
+ justify-content: space-between;
59
+ align-items: center;
60
+ margin-bottom: 16px;
61
+ padding-bottom: 12px;
62
+ border-bottom: 1px solid #333;
63
+ `;
64
+ const Button = theming_1.styled.button `
65
+ background: #3b82f6;
66
+ color: white;
67
+ border: none;
68
+ border-radius: 4px;
69
+ padding: 6px 12px;
70
+ cursor: pointer;
71
+ font-size: 12px;
72
+ margin-left: 8px;
73
+
74
+ &:hover {
75
+ background: #2563eb;
76
+ }
77
+ `;
78
+ const SpanCard = theming_1.styled.div `
79
+ background: #242424;
80
+ border: 1px solid ${props => props.status === 'ERROR' ? '#ef4444' : '#333'};
81
+ border-radius: 6px;
82
+ padding: 12px;
83
+ margin-bottom: 12px;
84
+ `;
85
+ const SpanHeader = theming_1.styled.div `
86
+ display: flex;
87
+ justify-content: space-between;
88
+ align-items: flex-start;
89
+ margin-bottom: 8px;
90
+ `;
91
+ const SpanName = theming_1.styled.div `
92
+ font-weight: 600;
93
+ color: #3b82f6;
94
+ font-size: 13px;
95
+ `;
96
+ const SpanDuration = theming_1.styled.div `
97
+ color: #10b981;
98
+ font-size: 11px;
99
+ `;
100
+ const Attributes = theming_1.styled.div `
101
+ margin-top: 8px;
102
+ padding: 8px;
103
+ background: #1a1a1a;
104
+ border-radius: 4px;
105
+ font-size: 11px;
106
+ `;
107
+ const AttributeRow = theming_1.styled.div `
108
+ padding: 2px 0;
109
+ color: #a0a0a0;
110
+
111
+ span:first-child {
112
+ color: #8b5cf6;
113
+ }
114
+ `;
115
+ const EventList = theming_1.styled.div `
116
+ margin-top: 8px;
117
+ `;
118
+ const Event = theming_1.styled.div `
119
+ padding: 6px 8px;
120
+ background: #1a1a1a;
121
+ border-left: 2px solid #f59e0b;
122
+ margin-bottom: 4px;
123
+ font-size: 11px;
124
+ `;
125
+ const EventName = theming_1.styled.div `
126
+ color: #f59e0b;
127
+ font-weight: 600;
128
+ margin-bottom: 4px;
129
+ `;
130
+ const ViewToggle = theming_1.styled.div `
131
+ display: flex;
132
+ gap: 8px;
133
+ margin-bottom: 16px;
134
+ `;
135
+ const ToggleButton = theming_1.styled.button `
136
+ background: ${props => props.active ? '#3b82f6' : '#333'};
137
+ color: ${props => props.active ? 'white' : '#a0a0a0'};
138
+ border: none;
139
+ border-radius: 4px;
140
+ padding: 6px 16px;
141
+ cursor: pointer;
142
+ font-size: 12px;
143
+
144
+ &:hover {
145
+ background: ${props => props.active ? '#2563eb' : '#404040'};
146
+ }
147
+ `;
148
+ const CanvasPlaceholder = theming_1.styled.div `
149
+ padding: 40px;
150
+ text-align: center;
151
+ color: #666;
152
+ border: 2px dashed #333;
153
+ border-radius: 8px;
154
+
155
+ h3 {
156
+ margin: 0 0 8px 0;
157
+ color: #999;
158
+ }
159
+
160
+ p {
161
+ margin: 0;
162
+ font-size: 12px;
163
+ }
164
+ `;
165
+ const TelemetryPanel = ({ active }) => {
166
+ const [spans, setSpans] = (0, react_1.useState)([]);
167
+ const [view, setView] = (0, react_1.useState)('timeline');
168
+ const emit = (0, manager_api_1.useChannel)({
169
+ [constants_1.EVENTS.TELEMETRY_UPDATE]: (newSpans) => {
170
+ console.log('[TelemetryPanel] Received update, spans:', newSpans.length, newSpans);
171
+ setSpans(newSpans);
172
+ },
173
+ });
174
+ console.log('[TelemetryPanel] Render, active:', active, 'spans:', spans.length);
175
+ const handleClear = () => {
176
+ emit(constants_1.EVENTS.CLEAR_TELEMETRY);
177
+ setSpans([]);
178
+ };
179
+ const handleExport = () => {
180
+ const json = JSON.stringify(spans, null, 2);
181
+ const blob = new Blob([json], { type: 'application/json' });
182
+ const url = URL.createObjectURL(blob);
183
+ const a = document.createElement('a');
184
+ a.href = url;
185
+ a.download = 'storybook-telemetry.json';
186
+ a.click();
187
+ URL.revokeObjectURL(url);
188
+ };
189
+ if (!active) {
190
+ return null;
191
+ }
192
+ return (react_1.default.createElement(components_1.AddonPanel, { active: active },
193
+ react_1.default.createElement(PanelContainer, null,
194
+ react_1.default.createElement(Header, null,
195
+ react_1.default.createElement("div", null,
196
+ react_1.default.createElement("strong", null, "OTEL Telemetry"),
197
+ react_1.default.createElement("div", { style: { fontSize: '11px', color: '#666', marginTop: '4px' } },
198
+ spans.length,
199
+ " span",
200
+ spans.length !== 1 ? 's' : '',
201
+ " collected")),
202
+ react_1.default.createElement("div", null,
203
+ react_1.default.createElement(Button, { onClick: handleClear }, "Clear"),
204
+ react_1.default.createElement(Button, { onClick: handleExport }, "Export JSON"))),
205
+ react_1.default.createElement(ViewToggle, null,
206
+ react_1.default.createElement(ToggleButton, { active: view === 'timeline', onClick: () => setView('timeline') }, "Timeline"),
207
+ react_1.default.createElement(ToggleButton, { active: view === 'canvas', onClick: () => setView('canvas') }, "Canvas")),
208
+ view === 'timeline' ? (react_1.default.createElement(react_1.default.Fragment, null, spans.length === 0 ? (react_1.default.createElement("div", { style: { textAlign: 'center', padding: '40px', color: '#666' } }, "No telemetry data yet. Interact with the story to capture spans.")) : (spans.map(span => (react_1.default.createElement(SpanCard, { key: span.id, status: span.status },
209
+ react_1.default.createElement(SpanHeader, null,
210
+ react_1.default.createElement(SpanName, null, span.name),
211
+ span.duration && (react_1.default.createElement(SpanDuration, null,
212
+ span.duration,
213
+ "ms"))),
214
+ Object.keys(span.attributes).length > 0 && (react_1.default.createElement(Attributes, null, Object.entries(span.attributes).map(([key, value]) => (react_1.default.createElement(AttributeRow, { key: key },
215
+ react_1.default.createElement("span", null,
216
+ key,
217
+ ":"),
218
+ " ",
219
+ String(value)))))),
220
+ span.events.length > 0 && (react_1.default.createElement(EventList, null, span.events.map((event, idx) => (react_1.default.createElement(Event, { key: idx },
221
+ react_1.default.createElement(EventName, null, event.name),
222
+ Object.entries(event.attributes).map(([key, value]) => (react_1.default.createElement(AttributeRow, { key: key },
223
+ react_1.default.createElement("span", null,
224
+ key,
225
+ ":"),
226
+ " ",
227
+ String(value))))))))),
228
+ span.errorMessage && (react_1.default.createElement("div", { style: { marginTop: '8px', color: '#ef4444', fontSize: '11px' } },
229
+ "Error: ",
230
+ span.errorMessage)))))))) : (react_1.default.createElement(CanvasPlaceholder, null,
231
+ react_1.default.createElement("h3", null, "Canvas Visualization"),
232
+ react_1.default.createElement("p", null, "Canvas view will visualize the telemetry flow using your GraphRenderer."),
233
+ react_1.default.createElement("p", { style: { marginTop: '8px', fontSize: '11px' } }, "To implement: Convert spans to canvas format and render with GraphRenderer"))))));
234
+ };
235
+ exports.TelemetryPanel = TelemetryPanel;
@@ -0,0 +1,8 @@
1
+ export declare const ADDON_ID = "storybook-addon-otel-telemetry";
2
+ export declare const PANEL_ID = "storybook-addon-otel-telemetry/panel";
3
+ export declare const PARAM_KEY = "otelTelemetry";
4
+ export declare const EVENTS: {
5
+ TELEMETRY_UPDATE: string;
6
+ CLEAR_TELEMETRY: string;
7
+ EXPORT_TELEMETRY: string;
8
+ };
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EVENTS = exports.PARAM_KEY = exports.PANEL_ID = exports.ADDON_ID = void 0;
4
+ exports.ADDON_ID = 'storybook-addon-otel-telemetry';
5
+ exports.PANEL_ID = `${exports.ADDON_ID}/panel`;
6
+ exports.PARAM_KEY = 'otelTelemetry';
7
+ exports.EVENTS = {
8
+ TELEMETRY_UPDATE: `${exports.ADDON_ID}/telemetry-update`,
9
+ CLEAR_TELEMETRY: `${exports.ADDON_ID}/clear-telemetry`,
10
+ EXPORT_TELEMETRY: `${exports.ADDON_ID}/export-telemetry`,
11
+ };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Story Decorator
3
+ *
4
+ * Wraps stories to automatically capture telemetry
5
+ */
6
+ import React from 'react';
7
+ import { TelemetryCollector } from './collector';
8
+ export declare const withTelemetry: (Story: any, context: any) => React.JSX.Element;
9
+ export declare function useTelemetry(): {
10
+ rootSpan: any;
11
+ collector: typeof TelemetryCollector;
12
+ };
13
+ export declare function useTelemetrySpan(name: string, attributes?: Record<string, string | number | boolean>, deps?: unknown[]): any;
14
+ export declare const decorators: ((Story: any, context: any) => React.JSX.Element)[];
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ /**
3
+ * Story Decorator
4
+ *
5
+ * Wraps stories to automatically capture telemetry
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.decorators = exports.withTelemetry = void 0;
42
+ exports.useTelemetry = useTelemetry;
43
+ exports.useTelemetrySpan = useTelemetrySpan;
44
+ const react_1 = __importStar(require("react"));
45
+ const collector_1 = require("./collector");
46
+ const constants_1 = require("./constants");
47
+ const withTelemetry = (Story, context) => {
48
+ const [rootSpan, setRootSpan] = (0, react_1.useState)(null);
49
+ // Check if telemetry is enabled for this story
50
+ const enabled = context.parameters[constants_1.PARAM_KEY]?.enabled !== false;
51
+ console.log('[Telemetry Decorator] Story:', context.title + '/' + context.name, 'Enabled:', enabled);
52
+ (0, react_1.useEffect)(() => {
53
+ if (!enabled)
54
+ return;
55
+ // Start root span for story
56
+ const span = collector_1.TelemetryCollector.startSpan(`Story: ${context.title}/${context.name}`, {
57
+ 'story.id': context.id,
58
+ 'story.title': context.title,
59
+ 'story.name': context.name,
60
+ });
61
+ setRootSpan(span);
62
+ collector_1.TelemetryCollector.addEvent(span, 'story.mounted', {
63
+ 'story.name': `${context.title}/${context.name}`,
64
+ });
65
+ return () => {
66
+ collector_1.TelemetryCollector.addEvent(span, 'story.unmounted', {});
67
+ collector_1.TelemetryCollector.endSpan(span);
68
+ };
69
+ }, [context.id, enabled]);
70
+ if (!enabled) {
71
+ return react_1.default.createElement(Story, null);
72
+ }
73
+ return (react_1.default.createElement(TelemetryProvider, { rootSpan: rootSpan },
74
+ react_1.default.createElement(Story, null)));
75
+ };
76
+ exports.withTelemetry = withTelemetry;
77
+ // Context to provide telemetry to story components
78
+ const TelemetryContext = react_1.default.createContext(null);
79
+ function TelemetryProvider({ children, rootSpan }) {
80
+ return (react_1.default.createElement(TelemetryContext.Provider, { value: { rootSpan, collector: collector_1.TelemetryCollector } }, children));
81
+ }
82
+ // Hook for stories to access telemetry
83
+ function useTelemetry() {
84
+ const context = react_1.default.useContext(TelemetryContext);
85
+ if (!context) {
86
+ throw new Error('useTelemetry must be used within a story with telemetry enabled');
87
+ }
88
+ return context;
89
+ }
90
+ // Hook for automatic span lifecycle
91
+ function useTelemetrySpan(name, attributes, deps = []) {
92
+ const { rootSpan } = useTelemetry();
93
+ const [span, setSpan] = (0, react_1.useState)(null);
94
+ (0, react_1.useEffect)(() => {
95
+ const newSpan = collector_1.TelemetryCollector.startSpan(name, attributes, rootSpan?.id);
96
+ setSpan(newSpan);
97
+ return () => {
98
+ collector_1.TelemetryCollector.endSpan(newSpan);
99
+ };
100
+ }, deps);
101
+ return span;
102
+ }
103
+ // Export decorators array for Storybook
104
+ exports.decorators = [exports.withTelemetry];
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Public API exports
3
+ */
4
+ export { TelemetryCollector } from './collector';
5
+ export { useTelemetry, useTelemetrySpan, withTelemetry } from './decorator';
6
+ export type { TelemetrySpan, SpanEvent } from './collector';
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ /**
3
+ * Public API exports
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.withTelemetry = exports.useTelemetrySpan = exports.useTelemetry = exports.TelemetryCollector = void 0;
7
+ var collector_1 = require("./collector");
8
+ Object.defineProperty(exports, "TelemetryCollector", { enumerable: true, get: function () { return collector_1.TelemetryCollector; } });
9
+ var decorator_1 = require("./decorator");
10
+ Object.defineProperty(exports, "useTelemetry", { enumerable: true, get: function () { return decorator_1.useTelemetry; } });
11
+ Object.defineProperty(exports, "useTelemetrySpan", { enumerable: true, get: function () { return decorator_1.useTelemetrySpan; } });
12
+ Object.defineProperty(exports, "withTelemetry", { enumerable: true, get: function () { return decorator_1.withTelemetry; } });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Addon Manager Registration
3
+ *
4
+ * Registers the telemetry panel in Storybook UI
5
+ */
6
+ export {};
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ /**
3
+ * Addon Manager Registration
4
+ *
5
+ * Registers the telemetry panel in Storybook UI
6
+ */
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ const react_1 = __importDefault(require("react"));
12
+ const manager_api_1 = require("@storybook/manager-api");
13
+ const TelemetryPanel_1 = require("./components/TelemetryPanel");
14
+ const constants_1 = require("./constants");
15
+ manager_api_1.addons.register(constants_1.ADDON_ID, (api) => {
16
+ manager_api_1.addons.add(constants_1.PANEL_ID, {
17
+ type: manager_api_1.types.PANEL,
18
+ title: 'OTEL Telemetry',
19
+ match: ({ viewMode }) => viewMode === 'story',
20
+ render: ({ active }) => react_1.default.createElement(TelemetryPanel_1.TelemetryPanel, { active: active || false }),
21
+ });
22
+ });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Addon Preset
3
+ *
4
+ * Configures the addon for Storybook
5
+ */
6
+ export declare function managerEntries(entry?: any[]): any[];
7
+ export declare function previewAnnotations(entry?: any[]): any[];
package/dist/preset.js ADDED
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ /**
3
+ * Addon Preset
4
+ *
5
+ * Configures the addon for Storybook
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.managerEntries = managerEntries;
9
+ exports.previewAnnotations = previewAnnotations;
10
+ function managerEntries(entry = []) {
11
+ return [...entry, require.resolve('./manager')];
12
+ }
13
+ function previewAnnotations(entry = []) {
14
+ return [...entry, require.resolve('./decorator')];
15
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Preview-side entry point (runs in browser)
3
+ * Must be ESM format
4
+ */
5
+
6
+ import React, { useEffect, useState } from 'react';
7
+
8
+ // Simple telemetry collector for browser
9
+ class BrowserTelemetryCollector {
10
+ constructor() {
11
+ this.spans = [];
12
+ this.spanIdCounter = 0;
13
+ this.activeSpans = new Map();
14
+
15
+ // Try to get Storybook channel (might not be available immediately)
16
+ if (typeof window !== 'undefined' && window.__STORYBOOK_ADDONS_CHANNEL__) {
17
+ this.channel = window.__STORYBOOK_ADDONS_CHANNEL__;
18
+ }
19
+ }
20
+
21
+ startSpan(name, attributes = {}, parentId) {
22
+ const span = {
23
+ id: `span-${++this.spanIdCounter}`,
24
+ name,
25
+ parentId,
26
+ startTime: Date.now(),
27
+ attributes: {
28
+ 'component.name': name,
29
+ ...attributes,
30
+ },
31
+ events: [],
32
+ status: 'OK',
33
+ };
34
+
35
+ this.spans.push(span);
36
+ this.activeSpans.set(span.id, span);
37
+
38
+ console.log('[TelemetryCollector] Started span:', name, 'Total spans:', this.spans.length);
39
+ this.emit();
40
+
41
+ return span;
42
+ }
43
+
44
+ addEvent(span, eventName, attributes = {}) {
45
+ span.events.push({
46
+ time: Date.now(),
47
+ name: eventName,
48
+ attributes,
49
+ });
50
+ this.emit();
51
+ }
52
+
53
+ endSpan(span) {
54
+ span.endTime = Date.now();
55
+ span.duration = span.endTime - span.startTime;
56
+ this.activeSpans.delete(span.id);
57
+ this.emit();
58
+ }
59
+
60
+ getSpans() {
61
+ return [...this.spans];
62
+ }
63
+
64
+ clear() {
65
+ this.spans = [];
66
+ this.spanIdCounter = 0;
67
+ this.activeSpans.clear();
68
+ this.emit();
69
+ }
70
+
71
+ emit() {
72
+ // Get channel if not already set
73
+ if (!this.channel && typeof window !== 'undefined' && window.__STORYBOOK_ADDONS_CHANNEL__) {
74
+ this.channel = window.__STORYBOOK_ADDONS_CHANNEL__;
75
+ }
76
+
77
+ console.log('[TelemetryCollector] Emitting update, spans:', this.spans.length, 'has channel:', !!this.channel);
78
+
79
+ if (this.channel) {
80
+ this.channel.emit('storybook-addon-otel-telemetry/telemetry-update', this.getSpans());
81
+ }
82
+ }
83
+ }
84
+
85
+ // Singleton instance
86
+ const TelemetryCollector = new BrowserTelemetryCollector();
87
+
88
+ // Decorator
89
+ export const withTelemetry = (Story, context) => {
90
+ const [rootSpan, setRootSpan] = useState(null);
91
+
92
+ console.log('[Telemetry Decorator] Story:', context.title + '/' + context.name);
93
+
94
+ useEffect(() => {
95
+ // Start root span for story
96
+ const span = TelemetryCollector.startSpan(`Story: ${context.title}/${context.name}`, {
97
+ 'story.id': context.id,
98
+ 'story.title': context.title,
99
+ 'story.name': context.name,
100
+ });
101
+
102
+ setRootSpan(span);
103
+
104
+ TelemetryCollector.addEvent(span, 'story.mounted', {
105
+ 'story.name': `${context.title}/${context.name}`,
106
+ });
107
+
108
+ return () => {
109
+ TelemetryCollector.addEvent(span, 'story.unmounted', {});
110
+ TelemetryCollector.endSpan(span);
111
+ };
112
+ }, [context.id]);
113
+
114
+ return React.createElement(Story);
115
+ };
116
+
117
+ // Export decorators array
118
+ export const decorators = [withTelemetry];
119
+
120
+ // Export for manual use
121
+ export { TelemetryCollector };
package/manager.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Manager entry point
3
+ * Re-exports from dist
4
+ */
5
+ module.exports = require('./dist/manager.js');
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@principal-ai/storybook-addon-otel",
3
+ "version": "0.1.0",
4
+ "description": "Storybook addon for capturing and visualizing OpenTelemetry data from story interactions",
5
+ "main": "dist/preset.js",
6
+ "types": "dist/preset.d.ts",
7
+ "files": [
8
+ "dist/**/*",
9
+ "preset.js",
10
+ "preview.js",
11
+ "manager.js",
12
+ "README.md"
13
+ ],
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/preset.d.ts",
17
+ "default": "./preset.js"
18
+ },
19
+ "./preset": {
20
+ "types": "./dist/preset.d.ts",
21
+ "default": "./preset.js"
22
+ },
23
+ "./preview": {
24
+ "types": "./dist/preview.d.ts",
25
+ "default": "./preview.js"
26
+ },
27
+ "./manager": {
28
+ "types": "./dist/manager.d.ts",
29
+ "default": "./dist/manager.js"
30
+ },
31
+ "./package.json": "./package.json"
32
+ },
33
+ "scripts": {
34
+ "build": "tsc && node scripts/copy-browser-files.js",
35
+ "dev": "tsc --watch",
36
+ "clean": "rm -rf dist"
37
+ },
38
+ "keywords": [
39
+ "storybook",
40
+ "storybook-addons",
41
+ "addon",
42
+ "telemetry",
43
+ "opentelemetry",
44
+ "otel",
45
+ "observability",
46
+ "visualization",
47
+ "canvas"
48
+ ],
49
+ "author": "Principal AI",
50
+ "license": "MIT",
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "https://github.com/principal-ai/principal-view-core-library",
54
+ "directory": "packages/storybook-addon-otel"
55
+ },
56
+ "peerDependencies": {
57
+ "@storybook/components": "^7.0.0 || ^8.0.0",
58
+ "@storybook/manager-api": "^7.0.0 || ^8.0.0",
59
+ "@storybook/preview-api": "^7.0.0 || ^8.0.0",
60
+ "@storybook/theming": "^7.0.0 || ^8.0.0",
61
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
62
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
63
+ },
64
+ "devDependencies": {
65
+ "@types/node": "^20.0.0",
66
+ "@types/react": "^18.0.0",
67
+ "typescript": "^5.0.0"
68
+ },
69
+ "publishConfig": {
70
+ "access": "public"
71
+ }
72
+ }
package/preset.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Storybook Preset
3
+ * Registers manager and preview entries for the addon
4
+ */
5
+
6
+ const path = require('path');
7
+
8
+ function managerEntries(entry = []) {
9
+ return [...entry, path.join(__dirname, 'dist', 'manager.js')];
10
+ }
11
+
12
+ function previewAnnotations(entry = []) {
13
+ return [...entry, path.join(__dirname, 'preview.js')];
14
+ }
15
+
16
+ module.exports = { managerEntries, previewAnnotations };
package/preview.js ADDED
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Preview-side entry point (runs in browser)
3
+ * Must be ESM format
4
+ */
5
+
6
+ import React, { useEffect, useState } from 'react';
7
+
8
+ // Simple telemetry collector for browser
9
+ class BrowserTelemetryCollector {
10
+ constructor() {
11
+ this.spans = [];
12
+ this.spanIdCounter = 0;
13
+ this.activeSpans = new Map();
14
+
15
+ // Try to get Storybook channel (might not be available immediately)
16
+ if (typeof window !== 'undefined' && window.__STORYBOOK_ADDONS_CHANNEL__) {
17
+ this.channel = window.__STORYBOOK_ADDONS_CHANNEL__;
18
+ }
19
+ }
20
+
21
+ startSpan(name, attributes = {}, parentId) {
22
+ const span = {
23
+ id: `span-${++this.spanIdCounter}`,
24
+ name,
25
+ parentId,
26
+ startTime: Date.now(),
27
+ attributes: {
28
+ 'component.name': name,
29
+ ...attributes,
30
+ },
31
+ events: [],
32
+ status: 'OK',
33
+ };
34
+
35
+ this.spans.push(span);
36
+ this.activeSpans.set(span.id, span);
37
+
38
+ console.log('[TelemetryCollector] Started span:', name, 'Total spans:', this.spans.length);
39
+ this.emit();
40
+
41
+ return span;
42
+ }
43
+
44
+ addEvent(span, eventName, attributes = {}) {
45
+ span.events.push({
46
+ time: Date.now(),
47
+ name: eventName,
48
+ attributes,
49
+ });
50
+ this.emit();
51
+ }
52
+
53
+ endSpan(span) {
54
+ span.endTime = Date.now();
55
+ span.duration = span.endTime - span.startTime;
56
+ this.activeSpans.delete(span.id);
57
+ this.emit();
58
+ }
59
+
60
+ getSpans() {
61
+ return [...this.spans];
62
+ }
63
+
64
+ clear() {
65
+ this.spans = [];
66
+ this.spanIdCounter = 0;
67
+ this.activeSpans.clear();
68
+ this.emit();
69
+ }
70
+
71
+ emit() {
72
+ // Get channel if not already set
73
+ if (!this.channel && typeof window !== 'undefined' && window.__STORYBOOK_ADDONS_CHANNEL__) {
74
+ this.channel = window.__STORYBOOK_ADDONS_CHANNEL__;
75
+ }
76
+
77
+ console.log('[TelemetryCollector] Emitting update, spans:', this.spans.length, 'has channel:', !!this.channel);
78
+
79
+ if (this.channel) {
80
+ this.channel.emit('storybook-addon-otel-telemetry/telemetry-update', this.getSpans());
81
+ }
82
+ }
83
+ }
84
+
85
+ // Singleton instance
86
+ const TelemetryCollector = new BrowserTelemetryCollector();
87
+
88
+ // Decorator
89
+ export const withTelemetry = (Story, context) => {
90
+ const [rootSpan, setRootSpan] = useState(null);
91
+
92
+ console.log('[Telemetry Decorator] Story:', context.title + '/' + context.name);
93
+
94
+ useEffect(() => {
95
+ // Start root span for story
96
+ const span = TelemetryCollector.startSpan(`Story: ${context.title}/${context.name}`, {
97
+ 'story.id': context.id,
98
+ 'story.title': context.title,
99
+ 'story.name': context.name,
100
+ });
101
+
102
+ setRootSpan(span);
103
+
104
+ TelemetryCollector.addEvent(span, 'story.mounted', {
105
+ 'story.name': `${context.title}/${context.name}`,
106
+ });
107
+
108
+ return () => {
109
+ TelemetryCollector.addEvent(span, 'story.unmounted', {});
110
+ TelemetryCollector.endSpan(span);
111
+ };
112
+ }, [context.id]);
113
+
114
+ return React.createElement(Story);
115
+ };
116
+
117
+ // Export decorators array
118
+ export const decorators = [withTelemetry];
119
+
120
+ // Export for manual use
121
+ export { TelemetryCollector };