@fallom/trace 0.1.0 → 0.1.3

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.mjs CHANGED
@@ -1,10 +1,4 @@
1
1
  var __defProp = Object.defineProperty;
2
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
- }) : x)(function(x) {
5
- if (typeof require !== "undefined") return require.apply(this, arguments);
6
- throw Error('Dynamic require of "' + x + '" is not supported');
7
- });
8
2
  var __export = (target, all) => {
9
3
  for (var name in all)
10
4
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -19,7 +13,10 @@ __export(trace_exports, {
19
13
  runWithSession: () => runWithSession,
20
14
  setSession: () => setSession,
21
15
  shutdown: () => shutdown,
22
- span: () => span
16
+ span: () => span,
17
+ wrapAnthropic: () => wrapAnthropic,
18
+ wrapGoogleAI: () => wrapGoogleAI,
19
+ wrapOpenAI: () => wrapOpenAI
23
20
  });
24
21
  import { AsyncLocalStorage } from "async_hooks";
25
22
  import { NodeSDK } from "@opentelemetry/sdk-node";
@@ -655,16 +652,25 @@ var apiKey = null;
655
652
  var baseUrl = "https://spans.fallom.com";
656
653
  var initialized = false;
657
654
  var captureContent = true;
655
+ var debugMode = false;
658
656
  var sdk = null;
657
+ function log(...args) {
658
+ if (debugMode) console.log("[Fallom]", ...args);
659
+ }
659
660
  var fallomSpanProcessor = {
660
661
  onStart(span2, _parentContext) {
662
+ log("\u{1F4CD} Span started:", span2.name || "unknown");
661
663
  const ctx = sessionStorage.getStore() || fallbackSession;
662
664
  if (ctx) {
663
665
  span2.setAttribute("fallom.config_key", ctx.configKey);
664
666
  span2.setAttribute("fallom.session_id", ctx.sessionId);
667
+ log(" Added session context:", ctx.configKey, ctx.sessionId);
668
+ } else {
669
+ log(" No session context available");
665
670
  }
666
671
  },
667
- onEnd(_span) {
672
+ onEnd(span2) {
673
+ log("\u2705 Span ended:", span2.name, "duration:", span2.duration);
668
674
  },
669
675
  shutdown() {
670
676
  return Promise.resolve();
@@ -673,8 +679,10 @@ var fallomSpanProcessor = {
673
679
  return Promise.resolve();
674
680
  }
675
681
  };
676
- function init(options = {}) {
682
+ async function init(options = {}) {
677
683
  if (initialized) return;
684
+ debugMode = options.debug ?? false;
685
+ log("\u{1F680} Initializing Fallom tracing...");
678
686
  apiKey = options.apiKey || process.env.FALLOM_API_KEY || null;
679
687
  baseUrl = options.baseUrl || process.env.FALLOM_BASE_URL || "https://spans.fallom.com";
680
688
  const envCapture = process.env.FALLOM_CAPTURE_CONTENT?.toLowerCase();
@@ -689,36 +697,52 @@ function init(options = {}) {
689
697
  );
690
698
  }
691
699
  initialized = true;
700
+ log("\u{1F4E1} Exporter URL:", `${baseUrl}/v1/traces`);
692
701
  const exporter = new OTLPTraceExporter({
693
702
  url: `${baseUrl}/v1/traces`,
694
703
  headers: {
695
704
  Authorization: `Bearer ${apiKey}`
696
705
  }
697
706
  });
707
+ const instrumentations = await getInstrumentations();
708
+ log("\u{1F527} Loaded instrumentations:", instrumentations.length);
698
709
  sdk = new NodeSDK({
699
710
  resource: new Resource({
700
711
  "service.name": "fallom-traced-app"
701
712
  }),
702
713
  traceExporter: exporter,
703
- spanProcessor: fallomSpanProcessor
714
+ spanProcessor: fallomSpanProcessor,
715
+ instrumentations
704
716
  });
705
717
  sdk.start();
706
- autoInstrument();
718
+ log("\u2705 SDK started");
707
719
  process.on("SIGTERM", () => {
708
720
  sdk?.shutdown().catch(console.error);
709
721
  });
710
722
  }
711
- function autoInstrument() {
723
+ async function getInstrumentations() {
724
+ const instrumentations = [];
725
+ await tryAddInstrumentation(instrumentations, "@traceloop/instrumentation-openai", "OpenAIInstrumentation");
726
+ await tryAddInstrumentation(instrumentations, "@traceloop/instrumentation-anthropic", "AnthropicInstrumentation");
727
+ await tryAddInstrumentation(instrumentations, "@traceloop/instrumentation-cohere", "CohereInstrumentation");
728
+ await tryAddInstrumentation(instrumentations, "@traceloop/instrumentation-bedrock", "BedrockInstrumentation");
729
+ await tryAddInstrumentation(instrumentations, "@traceloop/instrumentation-google-generativeai", "GoogleGenerativeAIInstrumentation");
730
+ await tryAddInstrumentation(instrumentations, "@traceloop/instrumentation-azure", "AzureOpenAIInstrumentation");
731
+ await tryAddInstrumentation(instrumentations, "@traceloop/instrumentation-vertexai", "VertexAIInstrumentation");
732
+ return instrumentations;
733
+ }
734
+ async function tryAddInstrumentation(instrumentations, pkg, className) {
712
735
  try {
713
- const { Traceloop } = __require("@traceloop/node-server-sdk");
714
- Traceloop.initialize({
715
- baseUrl: `${baseUrl}/v1/traces`,
716
- apiKey,
717
- disableBatch: false,
718
- // Respect capture content setting
719
- traceContent: captureContent
720
- });
721
- } catch {
736
+ const mod = await import(pkg);
737
+ const InstrumentationClass = mod[className] || mod.default?.[className];
738
+ if (InstrumentationClass) {
739
+ instrumentations.push(new InstrumentationClass({ traceContent: captureContent }));
740
+ log(` \u2705 Loaded ${pkg}`);
741
+ } else {
742
+ log(` \u26A0\uFE0F ${pkg} loaded but ${className} not found. Available:`, Object.keys(mod));
743
+ }
744
+ } catch (e) {
745
+ log(` \u274C ${pkg} not installed`);
722
746
  }
723
747
  }
724
748
  function setSession(configKey, sessionId) {
@@ -780,6 +804,169 @@ async function shutdown() {
780
804
  initialized = false;
781
805
  }
782
806
  }
807
+ async function sendTrace(trace) {
808
+ try {
809
+ const controller = new AbortController();
810
+ const timeoutId = setTimeout(() => controller.abort(), 5e3);
811
+ await fetch(`${baseUrl}/v1/traces`, {
812
+ method: "POST",
813
+ headers: {
814
+ Authorization: `Bearer ${apiKey}`,
815
+ "Content-Type": "application/json"
816
+ },
817
+ body: JSON.stringify(trace),
818
+ signal: controller.signal
819
+ });
820
+ clearTimeout(timeoutId);
821
+ log("\u{1F4E4} Trace sent:", trace.name, trace.model);
822
+ } catch {
823
+ }
824
+ }
825
+ function wrapOpenAI(client) {
826
+ const originalCreate = client.chat.completions.create.bind(client.chat.completions);
827
+ client.chat.completions.create = async function(...args) {
828
+ const ctx = sessionStorage.getStore() || fallbackSession;
829
+ if (!ctx || !initialized) {
830
+ return originalCreate(...args);
831
+ }
832
+ const params = args[0] || {};
833
+ const startTime = Date.now();
834
+ try {
835
+ const response = await originalCreate(...args);
836
+ const endTime = Date.now();
837
+ sendTrace({
838
+ config_key: ctx.configKey,
839
+ session_id: ctx.sessionId,
840
+ name: "chat.completions.create",
841
+ model: response?.model || params?.model,
842
+ start_time: new Date(startTime).toISOString(),
843
+ end_time: new Date(endTime).toISOString(),
844
+ duration_ms: endTime - startTime,
845
+ status: "OK",
846
+ prompt_tokens: response?.usage?.prompt_tokens,
847
+ completion_tokens: response?.usage?.completion_tokens,
848
+ total_tokens: response?.usage?.total_tokens,
849
+ input: captureContent ? JSON.stringify(params?.messages) : void 0,
850
+ output: captureContent ? response?.choices?.[0]?.message?.content : void 0
851
+ }).catch(() => {
852
+ });
853
+ return response;
854
+ } catch (error) {
855
+ const endTime = Date.now();
856
+ sendTrace({
857
+ config_key: ctx.configKey,
858
+ session_id: ctx.sessionId,
859
+ name: "chat.completions.create",
860
+ model: params?.model,
861
+ start_time: new Date(startTime).toISOString(),
862
+ end_time: new Date(endTime).toISOString(),
863
+ duration_ms: endTime - startTime,
864
+ status: "ERROR",
865
+ error_message: error?.message
866
+ }).catch(() => {
867
+ });
868
+ throw error;
869
+ }
870
+ };
871
+ return client;
872
+ }
873
+ function wrapAnthropic(client) {
874
+ const originalCreate = client.messages.create.bind(client.messages);
875
+ client.messages.create = async function(...args) {
876
+ const ctx = sessionStorage.getStore() || fallbackSession;
877
+ if (!ctx || !initialized) {
878
+ return originalCreate(...args);
879
+ }
880
+ const params = args[0] || {};
881
+ const startTime = Date.now();
882
+ try {
883
+ const response = await originalCreate(...args);
884
+ const endTime = Date.now();
885
+ sendTrace({
886
+ config_key: ctx.configKey,
887
+ session_id: ctx.sessionId,
888
+ name: "messages.create",
889
+ model: response?.model || params?.model,
890
+ start_time: new Date(startTime).toISOString(),
891
+ end_time: new Date(endTime).toISOString(),
892
+ duration_ms: endTime - startTime,
893
+ status: "OK",
894
+ prompt_tokens: response?.usage?.input_tokens,
895
+ completion_tokens: response?.usage?.output_tokens,
896
+ total_tokens: (response?.usage?.input_tokens || 0) + (response?.usage?.output_tokens || 0),
897
+ input: captureContent ? JSON.stringify(params?.messages) : void 0,
898
+ output: captureContent ? response?.content?.[0]?.text : void 0
899
+ }).catch(() => {
900
+ });
901
+ return response;
902
+ } catch (error) {
903
+ const endTime = Date.now();
904
+ sendTrace({
905
+ config_key: ctx.configKey,
906
+ session_id: ctx.sessionId,
907
+ name: "messages.create",
908
+ model: params?.model,
909
+ start_time: new Date(startTime).toISOString(),
910
+ end_time: new Date(endTime).toISOString(),
911
+ duration_ms: endTime - startTime,
912
+ status: "ERROR",
913
+ error_message: error?.message
914
+ }).catch(() => {
915
+ });
916
+ throw error;
917
+ }
918
+ };
919
+ return client;
920
+ }
921
+ function wrapGoogleAI(model) {
922
+ const originalGenerate = model.generateContent.bind(model);
923
+ model.generateContent = async function(...args) {
924
+ const ctx = sessionStorage.getStore() || fallbackSession;
925
+ if (!ctx || !initialized) {
926
+ return originalGenerate(...args);
927
+ }
928
+ const startTime = Date.now();
929
+ try {
930
+ const response = await originalGenerate(...args);
931
+ const endTime = Date.now();
932
+ const result = response?.response;
933
+ const usage = result?.usageMetadata;
934
+ sendTrace({
935
+ config_key: ctx.configKey,
936
+ session_id: ctx.sessionId,
937
+ name: "generateContent",
938
+ model: model?.model || "gemini",
939
+ start_time: new Date(startTime).toISOString(),
940
+ end_time: new Date(endTime).toISOString(),
941
+ duration_ms: endTime - startTime,
942
+ status: "OK",
943
+ prompt_tokens: usage?.promptTokenCount,
944
+ completion_tokens: usage?.candidatesTokenCount,
945
+ total_tokens: usage?.totalTokenCount,
946
+ input: captureContent ? JSON.stringify(args[0]) : void 0,
947
+ output: captureContent ? result?.text?.() : void 0
948
+ }).catch(() => {
949
+ });
950
+ return response;
951
+ } catch (error) {
952
+ const endTime = Date.now();
953
+ sendTrace({
954
+ config_key: ctx.configKey,
955
+ session_id: ctx.sessionId,
956
+ name: "generateContent",
957
+ model: model?.model || "gemini",
958
+ start_time: new Date(startTime).toISOString(),
959
+ end_time: new Date(endTime).toISOString(),
960
+ duration_ms: endTime - startTime,
961
+ status: "ERROR",
962
+ error_message: error?.message
963
+ }).catch(() => {
964
+ });
965
+ throw error;
966
+ }
967
+ };
968
+ return model;
969
+ }
783
970
 
784
971
  // src/models.ts
785
972
  var models_exports = {};
@@ -792,12 +979,12 @@ var apiKey2 = null;
792
979
  var baseUrl2 = "https://spans.fallom.com";
793
980
  var initialized2 = false;
794
981
  var syncInterval = null;
795
- var debugMode = false;
982
+ var debugMode2 = false;
796
983
  var configCache = /* @__PURE__ */ new Map();
797
984
  var SYNC_TIMEOUT = 2e3;
798
985
  var RECORD_TIMEOUT = 1e3;
799
- function log(msg) {
800
- if (debugMode) {
986
+ function log2(msg) {
987
+ if (debugMode2) {
801
988
  console.log(`[Fallom] ${msg}`);
802
989
  }
803
990
  }
@@ -828,11 +1015,11 @@ function ensureInit() {
828
1015
  }
829
1016
  async function fetchConfigs(timeout = SYNC_TIMEOUT) {
830
1017
  if (!apiKey2) {
831
- log("_fetchConfigs: No API key, skipping");
1018
+ log2("_fetchConfigs: No API key, skipping");
832
1019
  return;
833
1020
  }
834
1021
  try {
835
- log(`Fetching configs from ${baseUrl2}/configs`);
1022
+ log2(`Fetching configs from ${baseUrl2}/configs`);
836
1023
  const controller = new AbortController();
837
1024
  const timeoutId = setTimeout(() => controller.abort(), timeout);
838
1025
  const resp = await fetch(`${baseUrl2}/configs`, {
@@ -840,15 +1027,15 @@ async function fetchConfigs(timeout = SYNC_TIMEOUT) {
840
1027
  signal: controller.signal
841
1028
  });
842
1029
  clearTimeout(timeoutId);
843
- log(`Response status: ${resp.status}`);
1030
+ log2(`Response status: ${resp.status}`);
844
1031
  if (resp.ok) {
845
1032
  const data = await resp.json();
846
1033
  const configs = data.configs || [];
847
- log(`Got ${configs.length} configs: ${configs.map((c) => c.key)}`);
1034
+ log2(`Got ${configs.length} configs: ${configs.map((c) => c.key)}`);
848
1035
  for (const c of configs) {
849
1036
  const key = c.key;
850
1037
  const version = c.version || 1;
851
- log(`Config '${key}' v${version}: ${JSON.stringify(c.variants)}`);
1038
+ log2(`Config '${key}' v${version}: ${JSON.stringify(c.variants)}`);
852
1039
  if (!configCache.has(key)) {
853
1040
  configCache.set(key, { versions: /* @__PURE__ */ new Map(), latest: null });
854
1041
  }
@@ -857,10 +1044,10 @@ async function fetchConfigs(timeout = SYNC_TIMEOUT) {
857
1044
  cached.latest = version;
858
1045
  }
859
1046
  } else {
860
- log(`Fetch failed: ${resp.statusText}`);
1047
+ log2(`Fetch failed: ${resp.statusText}`);
861
1048
  }
862
1049
  } catch (e) {
863
- log(`Fetch exception: ${e}`);
1050
+ log2(`Fetch exception: ${e}`);
864
1051
  }
865
1052
  }
866
1053
  async function fetchSpecificVersion(configKey, version, timeout = SYNC_TIMEOUT) {
@@ -890,20 +1077,20 @@ async function fetchSpecificVersion(configKey, version, timeout = SYNC_TIMEOUT)
890
1077
  }
891
1078
  async function get(configKey, sessionId, options = {}) {
892
1079
  const { version, fallback, debug = false } = options;
893
- debugMode = debug;
1080
+ debugMode2 = debug;
894
1081
  ensureInit();
895
- log(`get() called: configKey=${configKey}, sessionId=${sessionId}, fallback=${fallback}`);
1082
+ log2(`get() called: configKey=${configKey}, sessionId=${sessionId}, fallback=${fallback}`);
896
1083
  try {
897
1084
  let configData = configCache.get(configKey);
898
- log(`Cache lookup for '${configKey}': ${configData ? "found" : "not found"}`);
1085
+ log2(`Cache lookup for '${configKey}': ${configData ? "found" : "not found"}`);
899
1086
  if (!configData) {
900
- log("Not in cache, fetching...");
1087
+ log2("Not in cache, fetching...");
901
1088
  await fetchConfigs(SYNC_TIMEOUT);
902
1089
  configData = configCache.get(configKey);
903
- log(`After fetch, cache lookup: ${configData ? "found" : "still not found"}`);
1090
+ log2(`After fetch, cache lookup: ${configData ? "found" : "still not found"}`);
904
1091
  }
905
1092
  if (!configData) {
906
- log(`Config not found, using fallback: ${fallback}`);
1093
+ log2(`Config not found, using fallback: ${fallback}`);
907
1094
  if (fallback) {
908
1095
  console.warn(`[Fallom WARNING] Config '${configKey}' not found, using fallback model: ${fallback}`);
909
1096
  return returnWithTrace(configKey, sessionId, fallback, 0);
@@ -941,22 +1128,22 @@ async function get(configKey, sessionId, options = {}) {
941
1128
  const variantsRaw = config.variants;
942
1129
  const configVersion = config.version || targetVersion;
943
1130
  const variants = Array.isArray(variantsRaw) ? variantsRaw : Object.values(variantsRaw);
944
- log(`Config found! Version: ${configVersion}, Variants: ${JSON.stringify(variants)}`);
1131
+ log2(`Config found! Version: ${configVersion}, Variants: ${JSON.stringify(variants)}`);
945
1132
  const hashBytes = createHash("md5").update(sessionId).digest();
946
1133
  const hashVal = hashBytes.readUInt32BE(0) % 1e6;
947
- log(`Session hash: ${hashVal} (out of 1,000,000)`);
1134
+ log2(`Session hash: ${hashVal} (out of 1,000,000)`);
948
1135
  let cumulative = 0;
949
1136
  let assignedModel = variants[variants.length - 1].model;
950
1137
  for (const v of variants) {
951
1138
  const oldCumulative = cumulative;
952
1139
  cumulative += v.weight * 1e4;
953
- log(`Variant ${v.model}: weight=${v.weight}%, range=${oldCumulative}-${cumulative}, hash=${hashVal}, match=${hashVal < cumulative}`);
1140
+ log2(`Variant ${v.model}: weight=${v.weight}%, range=${oldCumulative}-${cumulative}, hash=${hashVal}, match=${hashVal < cumulative}`);
954
1141
  if (hashVal < cumulative) {
955
1142
  assignedModel = v.model;
956
1143
  break;
957
1144
  }
958
1145
  }
959
- log(`\u2705 Assigned model: ${assignedModel}`);
1146
+ log2(`\u2705 Assigned model: ${assignedModel}`);
960
1147
  return returnWithTrace(configKey, sessionId, assignedModel, configVersion);
961
1148
  } catch (e) {
962
1149
  if (e instanceof Error && e.message.includes("not found")) {
@@ -1005,12 +1192,13 @@ async function recordSession(configKey, version, sessionId, model) {
1005
1192
  }
1006
1193
 
1007
1194
  // src/init.ts
1008
- function init3(options = {}) {
1195
+ async function init3(options = {}) {
1009
1196
  const baseUrl3 = options.baseUrl || process.env.FALLOM_BASE_URL || "https://spans.fallom.com";
1010
- init({
1197
+ await init({
1011
1198
  apiKey: options.apiKey,
1012
1199
  baseUrl: baseUrl3,
1013
- captureContent: options.captureContent
1200
+ captureContent: options.captureContent,
1201
+ debug: options.debug
1014
1202
  });
1015
1203
  init2({
1016
1204
  apiKey: options.apiKey,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fallom/trace",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "Model A/B testing and tracing for LLM applications. Zero latency, production-ready.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -20,7 +20,10 @@
20
20
  "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
21
21
  "test": "vitest",
22
22
  "lint": "eslint src/",
23
- "prepublishOnly": "npm run build"
23
+ "prepublishOnly": "npm run build",
24
+ "publish:patch": "npm version patch && npm publish --access public",
25
+ "publish:minor": "npm version minor && npm publish --access public",
26
+ "publish:major": "npm version major && npm publish --access public"
24
27
  },
25
28
  "keywords": [
26
29
  "llm",
@@ -44,7 +47,8 @@
44
47
  "@opentelemetry/sdk-node": "^0.46.0",
45
48
  "@opentelemetry/sdk-trace-node": "^1.19.0",
46
49
  "@opentelemetry/exporter-trace-otlp-http": "^0.46.0",
47
- "@traceloop/node-server-sdk": "^0.5.0"
50
+ "@traceloop/instrumentation-openai": "^0.11.0",
51
+ "tslib": "^2.6.0"
48
52
  },
49
53
  "devDependencies": {
50
54
  "@types/node": "^20.10.0",
@@ -56,4 +60,3 @@
56
60
  "node": ">=18.0.0"
57
61
  }
58
62
  }
59
-