@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 +220 -0
- package/dist/collector.d.ts +39 -0
- package/dist/collector.js +83 -0
- package/dist/components/TelemetryPanel.d.ts +11 -0
- package/dist/components/TelemetryPanel.js +235 -0
- package/dist/constants.d.ts +8 -0
- package/dist/constants.js +11 -0
- package/dist/decorator.d.ts +14 -0
- package/dist/decorator.js +104 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +12 -0
- package/dist/manager.d.ts +6 -0
- package/dist/manager.js +22 -0
- package/dist/preset.d.ts +7 -0
- package/dist/preset.js +15 -0
- package/dist/preview.js +121 -0
- package/manager.js +5 -0
- package/package.json +72 -0
- package/preset.js +16 -0
- package/preview.js +121 -0
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,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];
|
package/dist/index.d.ts
ADDED
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; } });
|
package/dist/manager.js
ADDED
|
@@ -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
|
+
});
|
package/dist/preset.d.ts
ADDED
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
|
+
}
|
package/dist/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 };
|
package/manager.js
ADDED
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 };
|