@fallom/trace 0.1.1 → 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,39 +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 traceloopModule = __require("@traceloop/node-server-sdk");
714
- const Traceloop = traceloopModule.Traceloop || traceloopModule.default?.Traceloop || traceloopModule;
715
- if (!Traceloop?.initialize) {
716
- return;
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));
717
743
  }
718
- Traceloop.initialize({
719
- baseUrl,
720
- apiKey,
721
- disableBatch: true,
722
- traceContent: captureContent
723
- });
724
- } catch {
744
+ } catch (e) {
745
+ log(` \u274C ${pkg} not installed`);
725
746
  }
726
747
  }
727
748
  function setSession(configKey, sessionId) {
@@ -783,6 +804,169 @@ async function shutdown() {
783
804
  initialized = false;
784
805
  }
785
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
+ }
786
970
 
787
971
  // src/models.ts
788
972
  var models_exports = {};
@@ -795,12 +979,12 @@ var apiKey2 = null;
795
979
  var baseUrl2 = "https://spans.fallom.com";
796
980
  var initialized2 = false;
797
981
  var syncInterval = null;
798
- var debugMode = false;
982
+ var debugMode2 = false;
799
983
  var configCache = /* @__PURE__ */ new Map();
800
984
  var SYNC_TIMEOUT = 2e3;
801
985
  var RECORD_TIMEOUT = 1e3;
802
- function log(msg) {
803
- if (debugMode) {
986
+ function log2(msg) {
987
+ if (debugMode2) {
804
988
  console.log(`[Fallom] ${msg}`);
805
989
  }
806
990
  }
@@ -831,11 +1015,11 @@ function ensureInit() {
831
1015
  }
832
1016
  async function fetchConfigs(timeout = SYNC_TIMEOUT) {
833
1017
  if (!apiKey2) {
834
- log("_fetchConfigs: No API key, skipping");
1018
+ log2("_fetchConfigs: No API key, skipping");
835
1019
  return;
836
1020
  }
837
1021
  try {
838
- log(`Fetching configs from ${baseUrl2}/configs`);
1022
+ log2(`Fetching configs from ${baseUrl2}/configs`);
839
1023
  const controller = new AbortController();
840
1024
  const timeoutId = setTimeout(() => controller.abort(), timeout);
841
1025
  const resp = await fetch(`${baseUrl2}/configs`, {
@@ -843,15 +1027,15 @@ async function fetchConfigs(timeout = SYNC_TIMEOUT) {
843
1027
  signal: controller.signal
844
1028
  });
845
1029
  clearTimeout(timeoutId);
846
- log(`Response status: ${resp.status}`);
1030
+ log2(`Response status: ${resp.status}`);
847
1031
  if (resp.ok) {
848
1032
  const data = await resp.json();
849
1033
  const configs = data.configs || [];
850
- log(`Got ${configs.length} configs: ${configs.map((c) => c.key)}`);
1034
+ log2(`Got ${configs.length} configs: ${configs.map((c) => c.key)}`);
851
1035
  for (const c of configs) {
852
1036
  const key = c.key;
853
1037
  const version = c.version || 1;
854
- log(`Config '${key}' v${version}: ${JSON.stringify(c.variants)}`);
1038
+ log2(`Config '${key}' v${version}: ${JSON.stringify(c.variants)}`);
855
1039
  if (!configCache.has(key)) {
856
1040
  configCache.set(key, { versions: /* @__PURE__ */ new Map(), latest: null });
857
1041
  }
@@ -860,10 +1044,10 @@ async function fetchConfigs(timeout = SYNC_TIMEOUT) {
860
1044
  cached.latest = version;
861
1045
  }
862
1046
  } else {
863
- log(`Fetch failed: ${resp.statusText}`);
1047
+ log2(`Fetch failed: ${resp.statusText}`);
864
1048
  }
865
1049
  } catch (e) {
866
- log(`Fetch exception: ${e}`);
1050
+ log2(`Fetch exception: ${e}`);
867
1051
  }
868
1052
  }
869
1053
  async function fetchSpecificVersion(configKey, version, timeout = SYNC_TIMEOUT) {
@@ -893,20 +1077,20 @@ async function fetchSpecificVersion(configKey, version, timeout = SYNC_TIMEOUT)
893
1077
  }
894
1078
  async function get(configKey, sessionId, options = {}) {
895
1079
  const { version, fallback, debug = false } = options;
896
- debugMode = debug;
1080
+ debugMode2 = debug;
897
1081
  ensureInit();
898
- log(`get() called: configKey=${configKey}, sessionId=${sessionId}, fallback=${fallback}`);
1082
+ log2(`get() called: configKey=${configKey}, sessionId=${sessionId}, fallback=${fallback}`);
899
1083
  try {
900
1084
  let configData = configCache.get(configKey);
901
- log(`Cache lookup for '${configKey}': ${configData ? "found" : "not found"}`);
1085
+ log2(`Cache lookup for '${configKey}': ${configData ? "found" : "not found"}`);
902
1086
  if (!configData) {
903
- log("Not in cache, fetching...");
1087
+ log2("Not in cache, fetching...");
904
1088
  await fetchConfigs(SYNC_TIMEOUT);
905
1089
  configData = configCache.get(configKey);
906
- log(`After fetch, cache lookup: ${configData ? "found" : "still not found"}`);
1090
+ log2(`After fetch, cache lookup: ${configData ? "found" : "still not found"}`);
907
1091
  }
908
1092
  if (!configData) {
909
- log(`Config not found, using fallback: ${fallback}`);
1093
+ log2(`Config not found, using fallback: ${fallback}`);
910
1094
  if (fallback) {
911
1095
  console.warn(`[Fallom WARNING] Config '${configKey}' not found, using fallback model: ${fallback}`);
912
1096
  return returnWithTrace(configKey, sessionId, fallback, 0);
@@ -944,22 +1128,22 @@ async function get(configKey, sessionId, options = {}) {
944
1128
  const variantsRaw = config.variants;
945
1129
  const configVersion = config.version || targetVersion;
946
1130
  const variants = Array.isArray(variantsRaw) ? variantsRaw : Object.values(variantsRaw);
947
- log(`Config found! Version: ${configVersion}, Variants: ${JSON.stringify(variants)}`);
1131
+ log2(`Config found! Version: ${configVersion}, Variants: ${JSON.stringify(variants)}`);
948
1132
  const hashBytes = createHash("md5").update(sessionId).digest();
949
1133
  const hashVal = hashBytes.readUInt32BE(0) % 1e6;
950
- log(`Session hash: ${hashVal} (out of 1,000,000)`);
1134
+ log2(`Session hash: ${hashVal} (out of 1,000,000)`);
951
1135
  let cumulative = 0;
952
1136
  let assignedModel = variants[variants.length - 1].model;
953
1137
  for (const v of variants) {
954
1138
  const oldCumulative = cumulative;
955
1139
  cumulative += v.weight * 1e4;
956
- 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}`);
957
1141
  if (hashVal < cumulative) {
958
1142
  assignedModel = v.model;
959
1143
  break;
960
1144
  }
961
1145
  }
962
- log(`\u2705 Assigned model: ${assignedModel}`);
1146
+ log2(`\u2705 Assigned model: ${assignedModel}`);
963
1147
  return returnWithTrace(configKey, sessionId, assignedModel, configVersion);
964
1148
  } catch (e) {
965
1149
  if (e instanceof Error && e.message.includes("not found")) {
@@ -1008,12 +1192,13 @@ async function recordSession(configKey, version, sessionId, model) {
1008
1192
  }
1009
1193
 
1010
1194
  // src/init.ts
1011
- function init3(options = {}) {
1195
+ async function init3(options = {}) {
1012
1196
  const baseUrl3 = options.baseUrl || process.env.FALLOM_BASE_URL || "https://spans.fallom.com";
1013
- init({
1197
+ await init({
1014
1198
  apiKey: options.apiKey,
1015
1199
  baseUrl: baseUrl3,
1016
- captureContent: options.captureContent
1200
+ captureContent: options.captureContent,
1201
+ debug: options.debug
1017
1202
  });
1018
1203
  init2({
1019
1204
  apiKey: options.apiKey,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fallom/trace",
3
- "version": "0.1.1",
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,7 @@
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",
48
51
  "tslib": "^2.6.0"
49
52
  },
50
53
  "devDependencies": {