@principal-ai/storybook-addon-otel 0.1.3 → 0.2.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.
Files changed (2) hide show
  1. package/manager.js +293 -5
  2. package/package.json +13 -5
package/manager.js CHANGED
@@ -1,17 +1,305 @@
1
1
  /**
2
2
  * Storybook OTEL Addon - Manager Entry (ESM for Storybook v10+)
3
3
  *
4
- * This ESM file is used by Storybook v10+ which requires ESM format.
4
+ * This ESM file is used by Storybook v10+ which uses the unified 'storybook' package.
5
5
  * The compiled dist/manager.js (CommonJS) is kept for backward compatibility with v7-8.
6
6
  */
7
7
 
8
- import { addons, types } from '@storybook/manager-api';
9
- import { TelemetryPanel } from './src/components/TelemetryPanel.tsx';
10
- import { ADDON_ID, PANEL_ID } from './src/constants.ts';
8
+ import React, { useEffect, useState } from 'react';
9
+ import { addons, types, useChannel } from 'storybook/manager-api';
10
+ import { AddonPanel } from 'storybook/internal/components';
11
+ import { styled } from 'storybook/theming';
12
+
13
+ const ADDON_ID = 'principal-ai/storybook-addon-otel';
14
+ const PANEL_ID = `${ADDON_ID}/panel`;
15
+ const EVENTS = {
16
+ TELEMETRY_UPDATE: `${ADDON_ID}/telemetry-update`,
17
+ CLEAR_TELEMETRY: `${ADDON_ID}/clear-telemetry`,
18
+ };
19
+
20
+ // Styled components
21
+ const PanelContainer = styled.div`
22
+ padding: 16px;
23
+ font-family: monospace;
24
+ font-size: 12px;
25
+ height: 100%;
26
+ overflow: auto;
27
+ background: #1a1a1a;
28
+ color: #e6e6e6;
29
+ `;
30
+
31
+ const Header = styled.div`
32
+ display: flex;
33
+ justify-content: space-between;
34
+ align-items: center;
35
+ margin-bottom: 16px;
36
+ padding-bottom: 12px;
37
+ border-bottom: 1px solid #333;
38
+ `;
39
+
40
+ const Button = styled.button`
41
+ background: #3b82f6;
42
+ color: white;
43
+ border: none;
44
+ border-radius: 4px;
45
+ padding: 6px 12px;
46
+ cursor: pointer;
47
+ font-size: 12px;
48
+ margin-left: 8px;
49
+
50
+ &:hover {
51
+ background: #2563eb;
52
+ }
53
+ `;
54
+
55
+ const SpanCard = styled.div`
56
+ background: #242424;
57
+ border: 1px solid ${props => props.status === 'ERROR' ? '#ef4444' : '#333'};
58
+ border-radius: 6px;
59
+ padding: 12px;
60
+ margin-bottom: 12px;
61
+ `;
62
+
63
+ const SpanHeader = styled.div`
64
+ display: flex;
65
+ justify-content: space-between;
66
+ align-items: flex-start;
67
+ margin-bottom: 8px;
68
+ `;
69
+
70
+ const SpanName = styled.div`
71
+ font-weight: 600;
72
+ color: #3b82f6;
73
+ font-size: 13px;
74
+ `;
75
+
76
+ const SpanDuration = styled.div`
77
+ color: #10b981;
78
+ font-size: 11px;
79
+ `;
80
+
81
+ const Attributes = styled.div`
82
+ margin-top: 8px;
83
+ padding: 8px;
84
+ background: #1a1a1a;
85
+ border-radius: 4px;
86
+ font-size: 11px;
87
+ `;
88
+
89
+ const AttributeRow = styled.div`
90
+ padding: 2px 0;
91
+ color: #a0a0a0;
92
+
93
+ span:first-child {
94
+ color: #8b5cf6;
95
+ }
96
+ `;
97
+
98
+ const EventList = styled.div`
99
+ margin-top: 8px;
100
+ `;
101
+
102
+ const Event = styled.div`
103
+ padding: 6px 8px;
104
+ background: #1a1a1a;
105
+ border-left: 2px solid #f59e0b;
106
+ margin-bottom: 4px;
107
+ font-size: 11px;
108
+ `;
109
+
110
+ const EventName = styled.div`
111
+ color: #f59e0b;
112
+ font-weight: 600;
113
+ margin-bottom: 4px;
114
+ `;
115
+
116
+ const ViewToggle = styled.div`
117
+ display: flex;
118
+ gap: 8px;
119
+ margin-bottom: 16px;
120
+ `;
121
+
122
+ const ToggleButton = styled.button`
123
+ background: ${props => props.active ? '#3b82f6' : '#333'};
124
+ color: ${props => props.active ? 'white' : '#a0a0a0'};
125
+ border: none;
126
+ border-radius: 4px;
127
+ padding: 6px 16px;
128
+ cursor: pointer;
129
+ font-size: 12px;
130
+
131
+ &:hover {
132
+ background: ${props => props.active ? '#2563eb' : '#404040'};
133
+ }
134
+ `;
135
+
136
+ const CanvasPlaceholder = styled.div`
137
+ padding: 40px;
138
+ text-align: center;
139
+ color: #666;
140
+ border: 2px dashed #333;
141
+ border-radius: 8px;
142
+
143
+ h3 {
144
+ margin: 0 0 8px 0;
145
+ color: #999;
146
+ }
147
+
148
+ p {
149
+ margin: 0;
150
+ font-size: 12px;
151
+ }
152
+ `;
153
+
154
+ // Telemetry Panel Component
155
+ const TelemetryPanel = ({ active }) => {
156
+ const [spans, setSpans] = useState([]);
157
+ const [view, setView] = useState('timeline');
158
+
159
+ const emit = useChannel({
160
+ [EVENTS.TELEMETRY_UPDATE]: (newSpans) => {
161
+ console.log('[TelemetryPanel] Received update, spans:', newSpans.length, newSpans);
162
+ setSpans(newSpans);
163
+ },
164
+ });
165
+
166
+ console.log('[TelemetryPanel] Render, active:', active, 'spans:', spans.length);
167
+
168
+ const handleClear = () => {
169
+ emit(EVENTS.CLEAR_TELEMETRY);
170
+ setSpans([]);
171
+ };
172
+
173
+ const handleExport = () => {
174
+ const json = JSON.stringify(spans, null, 2);
175
+ const blob = new Blob([json], { type: 'application/json' });
176
+ const url = URL.createObjectURL(blob);
177
+ const a = document.createElement('a');
178
+ a.href = url;
179
+ a.download = 'storybook-telemetry.json';
180
+ a.click();
181
+ URL.revokeObjectURL(url);
182
+ };
183
+
184
+ if (!active) {
185
+ return null;
186
+ }
187
+
188
+ return React.createElement(
189
+ AddonPanel,
190
+ { active },
191
+ React.createElement(
192
+ PanelContainer,
193
+ null,
194
+ React.createElement(
195
+ Header,
196
+ null,
197
+ React.createElement(
198
+ 'div',
199
+ null,
200
+ React.createElement('strong', null, 'OTEL Telemetry'),
201
+ React.createElement(
202
+ 'div',
203
+ { style: { fontSize: '11px', color: '#666', marginTop: '4px' } },
204
+ `${spans.length} span${spans.length !== 1 ? 's' : ''} collected`
205
+ )
206
+ ),
207
+ React.createElement(
208
+ 'div',
209
+ null,
210
+ React.createElement(Button, { onClick: handleClear }, 'Clear'),
211
+ React.createElement(Button, { onClick: handleExport }, 'Export JSON')
212
+ )
213
+ ),
214
+ React.createElement(
215
+ ViewToggle,
216
+ null,
217
+ React.createElement(
218
+ ToggleButton,
219
+ { active: view === 'timeline', onClick: () => setView('timeline') },
220
+ 'Timeline'
221
+ ),
222
+ React.createElement(
223
+ ToggleButton,
224
+ { active: view === 'canvas', onClick: () => setView('canvas') },
225
+ 'Canvas'
226
+ )
227
+ ),
228
+ view === 'timeline'
229
+ ? spans.length === 0
230
+ ? React.createElement(
231
+ 'div',
232
+ { style: { textAlign: 'center', padding: '40px', color: '#666' } },
233
+ 'No telemetry data yet. Interact with the story to capture spans.'
234
+ )
235
+ : spans.map(span =>
236
+ React.createElement(
237
+ SpanCard,
238
+ { key: span.id, status: span.status },
239
+ React.createElement(
240
+ SpanHeader,
241
+ null,
242
+ React.createElement(SpanName, null, span.name),
243
+ span.duration && React.createElement(SpanDuration, null, `${span.duration}ms`)
244
+ ),
245
+ Object.keys(span.attributes).length > 0 &&
246
+ React.createElement(
247
+ Attributes,
248
+ null,
249
+ Object.entries(span.attributes).map(([key, value]) =>
250
+ React.createElement(
251
+ AttributeRow,
252
+ { key },
253
+ React.createElement('span', null, `${key}:`),
254
+ ` ${String(value)}`
255
+ )
256
+ )
257
+ ),
258
+ span.events.length > 0 &&
259
+ React.createElement(
260
+ EventList,
261
+ null,
262
+ span.events.map((event, idx) =>
263
+ React.createElement(
264
+ Event,
265
+ { key: idx },
266
+ React.createElement(EventName, null, event.name),
267
+ Object.entries(event.attributes).map(([key, value]) =>
268
+ React.createElement(
269
+ AttributeRow,
270
+ { key },
271
+ React.createElement('span', null, `${key}:`),
272
+ ` ${String(value)}`
273
+ )
274
+ )
275
+ )
276
+ )
277
+ ),
278
+ span.errorMessage &&
279
+ React.createElement(
280
+ 'div',
281
+ { style: { marginTop: '8px', color: '#ef4444', fontSize: '11px' } },
282
+ `Error: ${span.errorMessage}`
283
+ )
284
+ )
285
+ )
286
+ : React.createElement(
287
+ CanvasPlaceholder,
288
+ null,
289
+ React.createElement('h3', null, 'Canvas Visualization'),
290
+ React.createElement('p', null, 'Canvas view will visualize the telemetry flow using your GraphRenderer.'),
291
+ React.createElement(
292
+ 'p',
293
+ { style: { marginTop: '8px', fontSize: '11px' } },
294
+ 'To implement: Convert spans to canvas format and render with GraphRenderer'
295
+ )
296
+ )
297
+ )
298
+ );
299
+ };
11
300
 
12
301
  // Register the addon
13
302
  addons.register(ADDON_ID, () => {
14
- // Register the panel
15
303
  addons.add(PANEL_ID, {
16
304
  type: types.PANEL,
17
305
  title: 'OTEL Telemetry',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principal-ai/storybook-addon-otel",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Storybook addon for capturing and visualizing OpenTelemetry data from story interactions",
5
5
  "main": "dist/preset.js",
6
6
  "types": "dist/preset.d.ts",
@@ -55,13 +55,21 @@
55
55
  "directory": "packages/storybook-addon-otel"
56
56
  },
57
57
  "peerDependencies": {
58
- "@storybook/components": "^7.0.0 || ^8.0.0 || ^10.0.0",
59
- "@storybook/manager-api": "^7.0.0 || ^8.0.0 || ^10.0.0",
60
- "@storybook/preview-api": "^7.0.0 || ^8.0.0 || ^10.0.0",
61
- "@storybook/theming": "^7.0.0 || ^8.0.0 || ^10.0.0",
58
+ "storybook": "^7.0.0 || ^8.0.0 || ^10.0.0",
62
59
  "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
63
60
  "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
64
61
  },
62
+ "peerDependenciesMeta": {
63
+ "@storybook/components": {
64
+ "optional": true
65
+ },
66
+ "@storybook/manager-api": {
67
+ "optional": true
68
+ },
69
+ "@storybook/theming": {
70
+ "optional": true
71
+ }
72
+ },
65
73
  "devDependencies": {
66
74
  "@types/node": "^20.0.0",
67
75
  "@types/react": "^18.0.0",