@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.js CHANGED
@@ -36,7 +36,10 @@ __export(trace_exports, {
36
36
  runWithSession: () => runWithSession,
37
37
  setSession: () => setSession,
38
38
  shutdown: () => shutdown,
39
- span: () => span
39
+ span: () => span,
40
+ wrapAnthropic: () => wrapAnthropic,
41
+ wrapGoogleAI: () => wrapGoogleAI,
42
+ wrapOpenAI: () => wrapOpenAI
40
43
  });
41
44
  var import_async_hooks = require("async_hooks");
42
45
  var import_sdk_node = require("@opentelemetry/sdk-node");
@@ -672,16 +675,25 @@ var apiKey = null;
672
675
  var baseUrl = "https://spans.fallom.com";
673
676
  var initialized = false;
674
677
  var captureContent = true;
678
+ var debugMode = false;
675
679
  var sdk = null;
680
+ function log(...args) {
681
+ if (debugMode) console.log("[Fallom]", ...args);
682
+ }
676
683
  var fallomSpanProcessor = {
677
684
  onStart(span2, _parentContext) {
685
+ log("\u{1F4CD} Span started:", span2.name || "unknown");
678
686
  const ctx = sessionStorage.getStore() || fallbackSession;
679
687
  if (ctx) {
680
688
  span2.setAttribute("fallom.config_key", ctx.configKey);
681
689
  span2.setAttribute("fallom.session_id", ctx.sessionId);
690
+ log(" Added session context:", ctx.configKey, ctx.sessionId);
691
+ } else {
692
+ log(" No session context available");
682
693
  }
683
694
  },
684
- onEnd(_span) {
695
+ onEnd(span2) {
696
+ log("\u2705 Span ended:", span2.name, "duration:", span2.duration);
685
697
  },
686
698
  shutdown() {
687
699
  return Promise.resolve();
@@ -690,8 +702,10 @@ var fallomSpanProcessor = {
690
702
  return Promise.resolve();
691
703
  }
692
704
  };
693
- function init(options = {}) {
705
+ async function init(options = {}) {
694
706
  if (initialized) return;
707
+ debugMode = options.debug ?? false;
708
+ log("\u{1F680} Initializing Fallom tracing...");
695
709
  apiKey = options.apiKey || process.env.FALLOM_API_KEY || null;
696
710
  baseUrl = options.baseUrl || process.env.FALLOM_BASE_URL || "https://spans.fallom.com";
697
711
  const envCapture = process.env.FALLOM_CAPTURE_CONTENT?.toLowerCase();
@@ -706,39 +720,52 @@ function init(options = {}) {
706
720
  );
707
721
  }
708
722
  initialized = true;
723
+ log("\u{1F4E1} Exporter URL:", `${baseUrl}/v1/traces`);
709
724
  const exporter = new import_exporter_trace_otlp_http.OTLPTraceExporter({
710
725
  url: `${baseUrl}/v1/traces`,
711
726
  headers: {
712
727
  Authorization: `Bearer ${apiKey}`
713
728
  }
714
729
  });
730
+ const instrumentations = await getInstrumentations();
731
+ log("\u{1F527} Loaded instrumentations:", instrumentations.length);
715
732
  sdk = new import_sdk_node.NodeSDK({
716
733
  resource: new Resource({
717
734
  "service.name": "fallom-traced-app"
718
735
  }),
719
736
  traceExporter: exporter,
720
- spanProcessor: fallomSpanProcessor
737
+ spanProcessor: fallomSpanProcessor,
738
+ instrumentations
721
739
  });
722
740
  sdk.start();
723
- autoInstrument();
741
+ log("\u2705 SDK started");
724
742
  process.on("SIGTERM", () => {
725
743
  sdk?.shutdown().catch(console.error);
726
744
  });
727
745
  }
728
- function autoInstrument() {
746
+ async function getInstrumentations() {
747
+ const instrumentations = [];
748
+ await tryAddInstrumentation(instrumentations, "@traceloop/instrumentation-openai", "OpenAIInstrumentation");
749
+ await tryAddInstrumentation(instrumentations, "@traceloop/instrumentation-anthropic", "AnthropicInstrumentation");
750
+ await tryAddInstrumentation(instrumentations, "@traceloop/instrumentation-cohere", "CohereInstrumentation");
751
+ await tryAddInstrumentation(instrumentations, "@traceloop/instrumentation-bedrock", "BedrockInstrumentation");
752
+ await tryAddInstrumentation(instrumentations, "@traceloop/instrumentation-google-generativeai", "GoogleGenerativeAIInstrumentation");
753
+ await tryAddInstrumentation(instrumentations, "@traceloop/instrumentation-azure", "AzureOpenAIInstrumentation");
754
+ await tryAddInstrumentation(instrumentations, "@traceloop/instrumentation-vertexai", "VertexAIInstrumentation");
755
+ return instrumentations;
756
+ }
757
+ async function tryAddInstrumentation(instrumentations, pkg, className) {
729
758
  try {
730
- const traceloopModule = require("@traceloop/node-server-sdk");
731
- const Traceloop = traceloopModule.Traceloop || traceloopModule.default?.Traceloop || traceloopModule;
732
- if (!Traceloop?.initialize) {
733
- return;
759
+ const mod = await import(pkg);
760
+ const InstrumentationClass = mod[className] || mod.default?.[className];
761
+ if (InstrumentationClass) {
762
+ instrumentations.push(new InstrumentationClass({ traceContent: captureContent }));
763
+ log(` \u2705 Loaded ${pkg}`);
764
+ } else {
765
+ log(` \u26A0\uFE0F ${pkg} loaded but ${className} not found. Available:`, Object.keys(mod));
734
766
  }
735
- Traceloop.initialize({
736
- baseUrl,
737
- apiKey,
738
- disableBatch: true,
739
- traceContent: captureContent
740
- });
741
- } catch {
767
+ } catch (e) {
768
+ log(` \u274C ${pkg} not installed`);
742
769
  }
743
770
  }
744
771
  function setSession(configKey, sessionId) {
@@ -800,6 +827,169 @@ async function shutdown() {
800
827
  initialized = false;
801
828
  }
802
829
  }
830
+ async function sendTrace(trace) {
831
+ try {
832
+ const controller = new AbortController();
833
+ const timeoutId = setTimeout(() => controller.abort(), 5e3);
834
+ await fetch(`${baseUrl}/v1/traces`, {
835
+ method: "POST",
836
+ headers: {
837
+ Authorization: `Bearer ${apiKey}`,
838
+ "Content-Type": "application/json"
839
+ },
840
+ body: JSON.stringify(trace),
841
+ signal: controller.signal
842
+ });
843
+ clearTimeout(timeoutId);
844
+ log("\u{1F4E4} Trace sent:", trace.name, trace.model);
845
+ } catch {
846
+ }
847
+ }
848
+ function wrapOpenAI(client) {
849
+ const originalCreate = client.chat.completions.create.bind(client.chat.completions);
850
+ client.chat.completions.create = async function(...args) {
851
+ const ctx = sessionStorage.getStore() || fallbackSession;
852
+ if (!ctx || !initialized) {
853
+ return originalCreate(...args);
854
+ }
855
+ const params = args[0] || {};
856
+ const startTime = Date.now();
857
+ try {
858
+ const response = await originalCreate(...args);
859
+ const endTime = Date.now();
860
+ sendTrace({
861
+ config_key: ctx.configKey,
862
+ session_id: ctx.sessionId,
863
+ name: "chat.completions.create",
864
+ model: response?.model || params?.model,
865
+ start_time: new Date(startTime).toISOString(),
866
+ end_time: new Date(endTime).toISOString(),
867
+ duration_ms: endTime - startTime,
868
+ status: "OK",
869
+ prompt_tokens: response?.usage?.prompt_tokens,
870
+ completion_tokens: response?.usage?.completion_tokens,
871
+ total_tokens: response?.usage?.total_tokens,
872
+ input: captureContent ? JSON.stringify(params?.messages) : void 0,
873
+ output: captureContent ? response?.choices?.[0]?.message?.content : void 0
874
+ }).catch(() => {
875
+ });
876
+ return response;
877
+ } catch (error) {
878
+ const endTime = Date.now();
879
+ sendTrace({
880
+ config_key: ctx.configKey,
881
+ session_id: ctx.sessionId,
882
+ name: "chat.completions.create",
883
+ model: params?.model,
884
+ start_time: new Date(startTime).toISOString(),
885
+ end_time: new Date(endTime).toISOString(),
886
+ duration_ms: endTime - startTime,
887
+ status: "ERROR",
888
+ error_message: error?.message
889
+ }).catch(() => {
890
+ });
891
+ throw error;
892
+ }
893
+ };
894
+ return client;
895
+ }
896
+ function wrapAnthropic(client) {
897
+ const originalCreate = client.messages.create.bind(client.messages);
898
+ client.messages.create = async function(...args) {
899
+ const ctx = sessionStorage.getStore() || fallbackSession;
900
+ if (!ctx || !initialized) {
901
+ return originalCreate(...args);
902
+ }
903
+ const params = args[0] || {};
904
+ const startTime = Date.now();
905
+ try {
906
+ const response = await originalCreate(...args);
907
+ const endTime = Date.now();
908
+ sendTrace({
909
+ config_key: ctx.configKey,
910
+ session_id: ctx.sessionId,
911
+ name: "messages.create",
912
+ model: response?.model || params?.model,
913
+ start_time: new Date(startTime).toISOString(),
914
+ end_time: new Date(endTime).toISOString(),
915
+ duration_ms: endTime - startTime,
916
+ status: "OK",
917
+ prompt_tokens: response?.usage?.input_tokens,
918
+ completion_tokens: response?.usage?.output_tokens,
919
+ total_tokens: (response?.usage?.input_tokens || 0) + (response?.usage?.output_tokens || 0),
920
+ input: captureContent ? JSON.stringify(params?.messages) : void 0,
921
+ output: captureContent ? response?.content?.[0]?.text : void 0
922
+ }).catch(() => {
923
+ });
924
+ return response;
925
+ } catch (error) {
926
+ const endTime = Date.now();
927
+ sendTrace({
928
+ config_key: ctx.configKey,
929
+ session_id: ctx.sessionId,
930
+ name: "messages.create",
931
+ model: params?.model,
932
+ start_time: new Date(startTime).toISOString(),
933
+ end_time: new Date(endTime).toISOString(),
934
+ duration_ms: endTime - startTime,
935
+ status: "ERROR",
936
+ error_message: error?.message
937
+ }).catch(() => {
938
+ });
939
+ throw error;
940
+ }
941
+ };
942
+ return client;
943
+ }
944
+ function wrapGoogleAI(model) {
945
+ const originalGenerate = model.generateContent.bind(model);
946
+ model.generateContent = async function(...args) {
947
+ const ctx = sessionStorage.getStore() || fallbackSession;
948
+ if (!ctx || !initialized) {
949
+ return originalGenerate(...args);
950
+ }
951
+ const startTime = Date.now();
952
+ try {
953
+ const response = await originalGenerate(...args);
954
+ const endTime = Date.now();
955
+ const result = response?.response;
956
+ const usage = result?.usageMetadata;
957
+ sendTrace({
958
+ config_key: ctx.configKey,
959
+ session_id: ctx.sessionId,
960
+ name: "generateContent",
961
+ model: model?.model || "gemini",
962
+ start_time: new Date(startTime).toISOString(),
963
+ end_time: new Date(endTime).toISOString(),
964
+ duration_ms: endTime - startTime,
965
+ status: "OK",
966
+ prompt_tokens: usage?.promptTokenCount,
967
+ completion_tokens: usage?.candidatesTokenCount,
968
+ total_tokens: usage?.totalTokenCount,
969
+ input: captureContent ? JSON.stringify(args[0]) : void 0,
970
+ output: captureContent ? result?.text?.() : void 0
971
+ }).catch(() => {
972
+ });
973
+ return response;
974
+ } catch (error) {
975
+ const endTime = Date.now();
976
+ sendTrace({
977
+ config_key: ctx.configKey,
978
+ session_id: ctx.sessionId,
979
+ name: "generateContent",
980
+ model: model?.model || "gemini",
981
+ start_time: new Date(startTime).toISOString(),
982
+ end_time: new Date(endTime).toISOString(),
983
+ duration_ms: endTime - startTime,
984
+ status: "ERROR",
985
+ error_message: error?.message
986
+ }).catch(() => {
987
+ });
988
+ throw error;
989
+ }
990
+ };
991
+ return model;
992
+ }
803
993
 
804
994
  // src/models.ts
805
995
  var models_exports = {};
@@ -812,12 +1002,12 @@ var apiKey2 = null;
812
1002
  var baseUrl2 = "https://spans.fallom.com";
813
1003
  var initialized2 = false;
814
1004
  var syncInterval = null;
815
- var debugMode = false;
1005
+ var debugMode2 = false;
816
1006
  var configCache = /* @__PURE__ */ new Map();
817
1007
  var SYNC_TIMEOUT = 2e3;
818
1008
  var RECORD_TIMEOUT = 1e3;
819
- function log(msg) {
820
- if (debugMode) {
1009
+ function log2(msg) {
1010
+ if (debugMode2) {
821
1011
  console.log(`[Fallom] ${msg}`);
822
1012
  }
823
1013
  }
@@ -848,11 +1038,11 @@ function ensureInit() {
848
1038
  }
849
1039
  async function fetchConfigs(timeout = SYNC_TIMEOUT) {
850
1040
  if (!apiKey2) {
851
- log("_fetchConfigs: No API key, skipping");
1041
+ log2("_fetchConfigs: No API key, skipping");
852
1042
  return;
853
1043
  }
854
1044
  try {
855
- log(`Fetching configs from ${baseUrl2}/configs`);
1045
+ log2(`Fetching configs from ${baseUrl2}/configs`);
856
1046
  const controller = new AbortController();
857
1047
  const timeoutId = setTimeout(() => controller.abort(), timeout);
858
1048
  const resp = await fetch(`${baseUrl2}/configs`, {
@@ -860,15 +1050,15 @@ async function fetchConfigs(timeout = SYNC_TIMEOUT) {
860
1050
  signal: controller.signal
861
1051
  });
862
1052
  clearTimeout(timeoutId);
863
- log(`Response status: ${resp.status}`);
1053
+ log2(`Response status: ${resp.status}`);
864
1054
  if (resp.ok) {
865
1055
  const data = await resp.json();
866
1056
  const configs = data.configs || [];
867
- log(`Got ${configs.length} configs: ${configs.map((c) => c.key)}`);
1057
+ log2(`Got ${configs.length} configs: ${configs.map((c) => c.key)}`);
868
1058
  for (const c of configs) {
869
1059
  const key = c.key;
870
1060
  const version = c.version || 1;
871
- log(`Config '${key}' v${version}: ${JSON.stringify(c.variants)}`);
1061
+ log2(`Config '${key}' v${version}: ${JSON.stringify(c.variants)}`);
872
1062
  if (!configCache.has(key)) {
873
1063
  configCache.set(key, { versions: /* @__PURE__ */ new Map(), latest: null });
874
1064
  }
@@ -877,10 +1067,10 @@ async function fetchConfigs(timeout = SYNC_TIMEOUT) {
877
1067
  cached.latest = version;
878
1068
  }
879
1069
  } else {
880
- log(`Fetch failed: ${resp.statusText}`);
1070
+ log2(`Fetch failed: ${resp.statusText}`);
881
1071
  }
882
1072
  } catch (e) {
883
- log(`Fetch exception: ${e}`);
1073
+ log2(`Fetch exception: ${e}`);
884
1074
  }
885
1075
  }
886
1076
  async function fetchSpecificVersion(configKey, version, timeout = SYNC_TIMEOUT) {
@@ -910,20 +1100,20 @@ async function fetchSpecificVersion(configKey, version, timeout = SYNC_TIMEOUT)
910
1100
  }
911
1101
  async function get(configKey, sessionId, options = {}) {
912
1102
  const { version, fallback, debug = false } = options;
913
- debugMode = debug;
1103
+ debugMode2 = debug;
914
1104
  ensureInit();
915
- log(`get() called: configKey=${configKey}, sessionId=${sessionId}, fallback=${fallback}`);
1105
+ log2(`get() called: configKey=${configKey}, sessionId=${sessionId}, fallback=${fallback}`);
916
1106
  try {
917
1107
  let configData = configCache.get(configKey);
918
- log(`Cache lookup for '${configKey}': ${configData ? "found" : "not found"}`);
1108
+ log2(`Cache lookup for '${configKey}': ${configData ? "found" : "not found"}`);
919
1109
  if (!configData) {
920
- log("Not in cache, fetching...");
1110
+ log2("Not in cache, fetching...");
921
1111
  await fetchConfigs(SYNC_TIMEOUT);
922
1112
  configData = configCache.get(configKey);
923
- log(`After fetch, cache lookup: ${configData ? "found" : "still not found"}`);
1113
+ log2(`After fetch, cache lookup: ${configData ? "found" : "still not found"}`);
924
1114
  }
925
1115
  if (!configData) {
926
- log(`Config not found, using fallback: ${fallback}`);
1116
+ log2(`Config not found, using fallback: ${fallback}`);
927
1117
  if (fallback) {
928
1118
  console.warn(`[Fallom WARNING] Config '${configKey}' not found, using fallback model: ${fallback}`);
929
1119
  return returnWithTrace(configKey, sessionId, fallback, 0);
@@ -961,22 +1151,22 @@ async function get(configKey, sessionId, options = {}) {
961
1151
  const variantsRaw = config.variants;
962
1152
  const configVersion = config.version || targetVersion;
963
1153
  const variants = Array.isArray(variantsRaw) ? variantsRaw : Object.values(variantsRaw);
964
- log(`Config found! Version: ${configVersion}, Variants: ${JSON.stringify(variants)}`);
1154
+ log2(`Config found! Version: ${configVersion}, Variants: ${JSON.stringify(variants)}`);
965
1155
  const hashBytes = (0, import_crypto.createHash)("md5").update(sessionId).digest();
966
1156
  const hashVal = hashBytes.readUInt32BE(0) % 1e6;
967
- log(`Session hash: ${hashVal} (out of 1,000,000)`);
1157
+ log2(`Session hash: ${hashVal} (out of 1,000,000)`);
968
1158
  let cumulative = 0;
969
1159
  let assignedModel = variants[variants.length - 1].model;
970
1160
  for (const v of variants) {
971
1161
  const oldCumulative = cumulative;
972
1162
  cumulative += v.weight * 1e4;
973
- log(`Variant ${v.model}: weight=${v.weight}%, range=${oldCumulative}-${cumulative}, hash=${hashVal}, match=${hashVal < cumulative}`);
1163
+ log2(`Variant ${v.model}: weight=${v.weight}%, range=${oldCumulative}-${cumulative}, hash=${hashVal}, match=${hashVal < cumulative}`);
974
1164
  if (hashVal < cumulative) {
975
1165
  assignedModel = v.model;
976
1166
  break;
977
1167
  }
978
1168
  }
979
- log(`\u2705 Assigned model: ${assignedModel}`);
1169
+ log2(`\u2705 Assigned model: ${assignedModel}`);
980
1170
  return returnWithTrace(configKey, sessionId, assignedModel, configVersion);
981
1171
  } catch (e) {
982
1172
  if (e instanceof Error && e.message.includes("not found")) {
@@ -1025,12 +1215,13 @@ async function recordSession(configKey, version, sessionId, model) {
1025
1215
  }
1026
1216
 
1027
1217
  // src/init.ts
1028
- function init3(options = {}) {
1218
+ async function init3(options = {}) {
1029
1219
  const baseUrl3 = options.baseUrl || process.env.FALLOM_BASE_URL || "https://spans.fallom.com";
1030
- init({
1220
+ await init({
1031
1221
  apiKey: options.apiKey,
1032
1222
  baseUrl: baseUrl3,
1033
- captureContent: options.captureContent
1223
+ captureContent: options.captureContent,
1224
+ debug: options.debug
1034
1225
  });
1035
1226
  init2({
1036
1227
  apiKey: options.apiKey,