@orka-js/devtools 1.1.0 → 1.2.1

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/dist/index.js CHANGED
@@ -709,6 +709,358 @@ function createTracerWithDevTools(options = {}) {
709
709
  };
710
710
  }
711
711
 
712
+ // src/opentelemetry.ts
713
+ var OpenTelemetryExporter = class {
714
+ config;
715
+ spanBuffer = [];
716
+ flushTimer;
717
+ unsubscribe;
718
+ constructor(config) {
719
+ this.config = {
720
+ endpoint: config.endpoint,
721
+ serviceName: config.serviceName ?? "orkajs-app",
722
+ serviceVersion: config.serviceVersion ?? "1.0.0",
723
+ headers: config.headers ?? {},
724
+ batchSize: config.batchSize ?? 100,
725
+ flushIntervalMs: config.flushIntervalMs ?? 5e3,
726
+ enabled: config.enabled ?? true
727
+ };
728
+ }
729
+ /**
730
+ * Start the exporter - subscribes to trace events
731
+ */
732
+ start() {
733
+ if (!this.config.enabled) return;
734
+ const collector = getCollector();
735
+ this.unsubscribe = collector.subscribe((event) => {
736
+ if (event.type === "run:end" && event.run) {
737
+ this.addSpan(event.run, event.sessionId);
738
+ }
739
+ });
740
+ this.flushTimer = setInterval(() => {
741
+ this.flush();
742
+ }, this.config.flushIntervalMs);
743
+ }
744
+ /**
745
+ * Stop the exporter
746
+ */
747
+ async stop() {
748
+ if (this.flushTimer) {
749
+ clearInterval(this.flushTimer);
750
+ this.flushTimer = void 0;
751
+ }
752
+ if (this.unsubscribe) {
753
+ this.unsubscribe();
754
+ this.unsubscribe = void 0;
755
+ }
756
+ await this.flush();
757
+ }
758
+ /**
759
+ * Convert a TraceRun to OTLP span
760
+ */
761
+ addSpan(run, traceId) {
762
+ const span = this.runToSpan(run, traceId);
763
+ this.spanBuffer.push(span);
764
+ for (const child of run.children) {
765
+ this.addSpan(child, traceId);
766
+ }
767
+ if (this.spanBuffer.length >= this.config.batchSize) {
768
+ this.flush();
769
+ }
770
+ }
771
+ /**
772
+ * Convert TraceRun to OTLP Span format
773
+ */
774
+ runToSpan(run, traceId) {
775
+ const attributes = [
776
+ { key: "orka.run.type", value: { stringValue: run.type } },
777
+ { key: "orka.run.name", value: { stringValue: run.name } },
778
+ { key: "orka.run.status", value: { stringValue: run.status } }
779
+ ];
780
+ if (run.metadata) {
781
+ if (run.metadata.model) {
782
+ attributes.push({ key: "llm.model", value: { stringValue: run.metadata.model } });
783
+ }
784
+ if (run.metadata.provider) {
785
+ attributes.push({ key: "llm.provider", value: { stringValue: run.metadata.provider } });
786
+ }
787
+ if (run.metadata.totalTokens !== void 0) {
788
+ attributes.push({ key: "llm.tokens.total", value: { intValue: String(run.metadata.totalTokens) } });
789
+ }
790
+ if (run.metadata.promptTokens !== void 0) {
791
+ attributes.push({ key: "llm.tokens.prompt", value: { intValue: String(run.metadata.promptTokens) } });
792
+ }
793
+ if (run.metadata.completionTokens !== void 0) {
794
+ attributes.push({ key: "llm.tokens.completion", value: { intValue: String(run.metadata.completionTokens) } });
795
+ }
796
+ if (run.metadata.cost !== void 0) {
797
+ attributes.push({ key: "llm.cost", value: { doubleValue: run.metadata.cost } });
798
+ }
799
+ if (run.metadata.toolName) {
800
+ attributes.push({ key: "tool.name", value: { stringValue: run.metadata.toolName } });
801
+ }
802
+ }
803
+ if (run.latencyMs !== void 0) {
804
+ attributes.push({ key: "orka.latency_ms", value: { intValue: String(run.latencyMs) } });
805
+ }
806
+ if (run.error) {
807
+ attributes.push({ key: "error.message", value: { stringValue: run.error } });
808
+ }
809
+ return {
810
+ traceId: this.toHex(traceId, 32),
811
+ spanId: this.toHex(run.id, 16),
812
+ parentSpanId: run.parentId ? this.toHex(run.parentId, 16) : void 0,
813
+ name: `${run.type}/${run.name}`,
814
+ kind: this.getSpanKind(run.type),
815
+ startTimeUnixNano: String(run.startTime * 1e6),
816
+ endTimeUnixNano: String((run.endTime ?? run.startTime) * 1e6),
817
+ attributes,
818
+ status: {
819
+ code: run.status === "error" ? 2 : 1,
820
+ message: run.error
821
+ }
822
+ };
823
+ }
824
+ /**
825
+ * Get OTLP span kind based on run type
826
+ */
827
+ getSpanKind(type) {
828
+ switch (type) {
829
+ case "llm":
830
+ case "embedding":
831
+ return 3;
832
+ // CLIENT
833
+ case "tool":
834
+ return 3;
835
+ // CLIENT
836
+ case "agent":
837
+ case "chain":
838
+ case "workflow":
839
+ case "graph":
840
+ return 0;
841
+ // INTERNAL
842
+ default:
843
+ return 0;
844
+ }
845
+ }
846
+ /**
847
+ * Convert string ID to hex format
848
+ */
849
+ toHex(id, length) {
850
+ let hash = 0;
851
+ for (let i = 0; i < id.length; i++) {
852
+ const char = id.charCodeAt(i);
853
+ hash = (hash << 5) - hash + char;
854
+ hash = hash & hash;
855
+ }
856
+ const hex = Math.abs(hash).toString(16).padStart(length, "0");
857
+ return hex.slice(0, length);
858
+ }
859
+ /**
860
+ * Flush spans to OTLP endpoint
861
+ */
862
+ async flush() {
863
+ if (this.spanBuffer.length === 0) return;
864
+ const spans = [...this.spanBuffer];
865
+ this.spanBuffer = [];
866
+ const payload = {
867
+ resourceSpans: [{
868
+ resource: {
869
+ attributes: [
870
+ { key: "service.name", value: { stringValue: this.config.serviceName } },
871
+ { key: "service.version", value: { stringValue: this.config.serviceVersion } },
872
+ { key: "telemetry.sdk.name", value: { stringValue: "@orka-js/devtools" } },
873
+ { key: "telemetry.sdk.version", value: { stringValue: "1.1.0" } }
874
+ ]
875
+ },
876
+ scopeSpans: [{
877
+ scope: {
878
+ name: "@orka-js/devtools",
879
+ version: "1.1.0"
880
+ },
881
+ spans
882
+ }]
883
+ }]
884
+ };
885
+ try {
886
+ const response = await fetch(`${this.config.endpoint}/v1/traces`, {
887
+ method: "POST",
888
+ headers: {
889
+ "Content-Type": "application/json",
890
+ ...this.config.headers
891
+ },
892
+ body: JSON.stringify(payload)
893
+ });
894
+ if (!response.ok) {
895
+ console.error(`[DevTools] OTLP export failed: ${response.status} ${response.statusText}`);
896
+ this.spanBuffer.unshift(...spans);
897
+ }
898
+ } catch (error) {
899
+ console.error("[DevTools] OTLP export error:", error);
900
+ this.spanBuffer.unshift(...spans);
901
+ }
902
+ }
903
+ };
904
+ function createOTLPExporter(config) {
905
+ const exporter = new OpenTelemetryExporter(config);
906
+ exporter.start();
907
+ return exporter;
908
+ }
909
+
910
+ // src/replay.ts
911
+ var ReplayDebugger = class {
912
+ /**
913
+ * Replay a trace run with optionally modified input
914
+ */
915
+ async replay(options) {
916
+ const collector = getCollector();
917
+ const originalRun = collector.findRun(options.runId, options.sessionId);
918
+ if (!originalRun) {
919
+ throw new Error(`Run not found: ${options.runId}`);
920
+ }
921
+ const modifiedInput = options.modifyInput ? options.modifyInput(originalRun.input) : originalRun.input;
922
+ const replaySessionId = collector.startSession(`Replay: ${originalRun.name}`);
923
+ const replayRunId = collector.startRun(
924
+ originalRun.type,
925
+ `replay:${originalRun.name}`,
926
+ modifiedInput,
927
+ {
928
+ ...originalRun.metadata,
929
+ replayOf: originalRun.id,
930
+ originalSessionId: options.sessionId
931
+ }
932
+ );
933
+ const replayedOutput = originalRun.output;
934
+ collector.endRun(replayRunId, replayedOutput, originalRun.metadata);
935
+ collector.endSession(replaySessionId);
936
+ const replayedRun = collector.findRun(replayRunId);
937
+ if (!replayedRun) {
938
+ throw new Error("Failed to create replayed run");
939
+ }
940
+ const inputChanged = JSON.stringify(originalRun.input) !== JSON.stringify(modifiedInput);
941
+ const outputChanged = JSON.stringify(originalRun.output) !== JSON.stringify(replayedRun.output);
942
+ const latencyDiff = (replayedRun.latencyMs ?? 0) - (originalRun.latencyMs ?? 0);
943
+ return {
944
+ originalRun,
945
+ replayedRun,
946
+ diff: {
947
+ inputChanged,
948
+ outputChanged,
949
+ latencyDiff
950
+ }
951
+ };
952
+ }
953
+ /**
954
+ * Fork a trace to create a new branch for experimentation
955
+ */
956
+ fork(runId, sessionId) {
957
+ const collector = getCollector();
958
+ const originalRun = collector.findRun(runId, sessionId);
959
+ if (!originalRun) {
960
+ throw new Error(`Run not found: ${runId}`);
961
+ }
962
+ const forkSessionId = collector.startSession(`Fork: ${originalRun.name}`);
963
+ this.cloneRunTree(originalRun, forkSessionId);
964
+ collector.endSession(forkSessionId);
965
+ return forkSessionId;
966
+ }
967
+ /**
968
+ * Clone a run and its children
969
+ */
970
+ cloneRunTree(run, _sessionId, parentId) {
971
+ const collector = getCollector();
972
+ const newRunId = collector.startRun(
973
+ run.type,
974
+ `fork:${run.name}`,
975
+ run.input,
976
+ {
977
+ ...run.metadata,
978
+ forkedFrom: run.id,
979
+ originalParentId: parentId
980
+ }
981
+ );
982
+ for (const child of run.children) {
983
+ this.cloneRunTree(child, _sessionId, newRunId);
984
+ }
985
+ if (run.status === "error") {
986
+ collector.errorRun(newRunId, run.error ?? "Unknown error");
987
+ } else {
988
+ collector.endRun(newRunId, run.output, run.metadata);
989
+ }
990
+ return newRunId;
991
+ }
992
+ /**
993
+ * Compare two runs and return detailed diff
994
+ */
995
+ compare(runId1, runId2, sessionId) {
996
+ const collector = getCollector();
997
+ const run1 = collector.findRun(runId1, sessionId);
998
+ const run2 = collector.findRun(runId2, sessionId);
999
+ if (!run1 || !run2) {
1000
+ throw new Error("One or both runs not found");
1001
+ }
1002
+ return {
1003
+ run1: {
1004
+ id: run1.id,
1005
+ name: run1.name,
1006
+ type: run1.type,
1007
+ latencyMs: run1.latencyMs,
1008
+ status: run1.status,
1009
+ tokenCount: run1.metadata?.totalTokens
1010
+ },
1011
+ run2: {
1012
+ id: run2.id,
1013
+ name: run2.name,
1014
+ type: run2.type,
1015
+ latencyMs: run2.latencyMs,
1016
+ status: run2.status,
1017
+ tokenCount: run2.metadata?.totalTokens
1018
+ },
1019
+ diff: {
1020
+ latencyDiff: (run2.latencyMs ?? 0) - (run1.latencyMs ?? 0),
1021
+ latencyDiffPercent: run1.latencyMs ? ((run2.latencyMs ?? 0) - run1.latencyMs) / run1.latencyMs * 100 : 0,
1022
+ tokenDiff: (run2.metadata?.totalTokens ?? 0) - (run1.metadata?.totalTokens ?? 0),
1023
+ statusChanged: run1.status !== run2.status,
1024
+ inputChanged: JSON.stringify(run1.input) !== JSON.stringify(run2.input),
1025
+ outputChanged: JSON.stringify(run1.output) !== JSON.stringify(run2.output)
1026
+ }
1027
+ };
1028
+ }
1029
+ /**
1030
+ * Export a run as a reproducible test case
1031
+ */
1032
+ exportTestCase(runId, sessionId) {
1033
+ const collector = getCollector();
1034
+ const run = collector.findRun(runId, sessionId);
1035
+ if (!run) {
1036
+ throw new Error(`Run not found: ${runId}`);
1037
+ }
1038
+ return {
1039
+ name: `test_${run.name}_${run.id.slice(0, 8)}`,
1040
+ description: `Exported from DevTools trace ${run.id}`,
1041
+ type: run.type,
1042
+ input: run.input,
1043
+ expectedOutput: run.output,
1044
+ metadata: run.metadata,
1045
+ assertions: [
1046
+ { type: "status", expected: run.status },
1047
+ { type: "latency_max", expected: (run.latencyMs ?? 0) * 1.5 }
1048
+ ],
1049
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1050
+ };
1051
+ }
1052
+ };
1053
+ function createReplayDebugger() {
1054
+ return new ReplayDebugger();
1055
+ }
1056
+ var replayDebugger;
1057
+ function getReplayDebugger() {
1058
+ if (!replayDebugger) {
1059
+ replayDebugger = new ReplayDebugger();
1060
+ }
1061
+ return replayDebugger;
1062
+ }
1063
+
712
1064
  // src/index.ts
713
1065
  async function devtools(config = {}) {
714
1066
  const collector = getCollector(config);
@@ -798,12 +1150,17 @@ var trace = {
798
1150
  };
799
1151
  export {
800
1152
  DevToolsServer,
1153
+ OpenTelemetryExporter,
1154
+ ReplayDebugger,
801
1155
  Trace,
802
1156
  TraceCollector,
803
1157
  createDevToolsHook,
1158
+ createOTLPExporter,
1159
+ createReplayDebugger,
804
1160
  createTracerWithDevTools,
805
1161
  devtools,
806
1162
  getCollector,
1163
+ getReplayDebugger,
807
1164
  resetCollector,
808
1165
  trace,
809
1166
  withTrace