@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.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,36 +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 { Traceloop } = require("@traceloop/node-server-sdk");
731
- Traceloop.initialize({
732
- baseUrl: `${baseUrl}/v1/traces`,
733
- apiKey,
734
- disableBatch: false,
735
- // Respect capture content setting
736
- traceContent: captureContent
737
- });
738
- } catch {
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));
766
+ }
767
+ } catch (e) {
768
+ log(` \u274C ${pkg} not installed`);
739
769
  }
740
770
  }
741
771
  function setSession(configKey, sessionId) {
@@ -797,6 +827,169 @@ async function shutdown() {
797
827
  initialized = false;
798
828
  }
799
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
+ }
800
993
 
801
994
  // src/models.ts
802
995
  var models_exports = {};
@@ -809,12 +1002,12 @@ var apiKey2 = null;
809
1002
  var baseUrl2 = "https://spans.fallom.com";
810
1003
  var initialized2 = false;
811
1004
  var syncInterval = null;
812
- var debugMode = false;
1005
+ var debugMode2 = false;
813
1006
  var configCache = /* @__PURE__ */ new Map();
814
1007
  var SYNC_TIMEOUT = 2e3;
815
1008
  var RECORD_TIMEOUT = 1e3;
816
- function log(msg) {
817
- if (debugMode) {
1009
+ function log2(msg) {
1010
+ if (debugMode2) {
818
1011
  console.log(`[Fallom] ${msg}`);
819
1012
  }
820
1013
  }
@@ -845,11 +1038,11 @@ function ensureInit() {
845
1038
  }
846
1039
  async function fetchConfigs(timeout = SYNC_TIMEOUT) {
847
1040
  if (!apiKey2) {
848
- log("_fetchConfigs: No API key, skipping");
1041
+ log2("_fetchConfigs: No API key, skipping");
849
1042
  return;
850
1043
  }
851
1044
  try {
852
- log(`Fetching configs from ${baseUrl2}/configs`);
1045
+ log2(`Fetching configs from ${baseUrl2}/configs`);
853
1046
  const controller = new AbortController();
854
1047
  const timeoutId = setTimeout(() => controller.abort(), timeout);
855
1048
  const resp = await fetch(`${baseUrl2}/configs`, {
@@ -857,15 +1050,15 @@ async function fetchConfigs(timeout = SYNC_TIMEOUT) {
857
1050
  signal: controller.signal
858
1051
  });
859
1052
  clearTimeout(timeoutId);
860
- log(`Response status: ${resp.status}`);
1053
+ log2(`Response status: ${resp.status}`);
861
1054
  if (resp.ok) {
862
1055
  const data = await resp.json();
863
1056
  const configs = data.configs || [];
864
- log(`Got ${configs.length} configs: ${configs.map((c) => c.key)}`);
1057
+ log2(`Got ${configs.length} configs: ${configs.map((c) => c.key)}`);
865
1058
  for (const c of configs) {
866
1059
  const key = c.key;
867
1060
  const version = c.version || 1;
868
- log(`Config '${key}' v${version}: ${JSON.stringify(c.variants)}`);
1061
+ log2(`Config '${key}' v${version}: ${JSON.stringify(c.variants)}`);
869
1062
  if (!configCache.has(key)) {
870
1063
  configCache.set(key, { versions: /* @__PURE__ */ new Map(), latest: null });
871
1064
  }
@@ -874,10 +1067,10 @@ async function fetchConfigs(timeout = SYNC_TIMEOUT) {
874
1067
  cached.latest = version;
875
1068
  }
876
1069
  } else {
877
- log(`Fetch failed: ${resp.statusText}`);
1070
+ log2(`Fetch failed: ${resp.statusText}`);
878
1071
  }
879
1072
  } catch (e) {
880
- log(`Fetch exception: ${e}`);
1073
+ log2(`Fetch exception: ${e}`);
881
1074
  }
882
1075
  }
883
1076
  async function fetchSpecificVersion(configKey, version, timeout = SYNC_TIMEOUT) {
@@ -907,20 +1100,20 @@ async function fetchSpecificVersion(configKey, version, timeout = SYNC_TIMEOUT)
907
1100
  }
908
1101
  async function get(configKey, sessionId, options = {}) {
909
1102
  const { version, fallback, debug = false } = options;
910
- debugMode = debug;
1103
+ debugMode2 = debug;
911
1104
  ensureInit();
912
- log(`get() called: configKey=${configKey}, sessionId=${sessionId}, fallback=${fallback}`);
1105
+ log2(`get() called: configKey=${configKey}, sessionId=${sessionId}, fallback=${fallback}`);
913
1106
  try {
914
1107
  let configData = configCache.get(configKey);
915
- log(`Cache lookup for '${configKey}': ${configData ? "found" : "not found"}`);
1108
+ log2(`Cache lookup for '${configKey}': ${configData ? "found" : "not found"}`);
916
1109
  if (!configData) {
917
- log("Not in cache, fetching...");
1110
+ log2("Not in cache, fetching...");
918
1111
  await fetchConfigs(SYNC_TIMEOUT);
919
1112
  configData = configCache.get(configKey);
920
- log(`After fetch, cache lookup: ${configData ? "found" : "still not found"}`);
1113
+ log2(`After fetch, cache lookup: ${configData ? "found" : "still not found"}`);
921
1114
  }
922
1115
  if (!configData) {
923
- log(`Config not found, using fallback: ${fallback}`);
1116
+ log2(`Config not found, using fallback: ${fallback}`);
924
1117
  if (fallback) {
925
1118
  console.warn(`[Fallom WARNING] Config '${configKey}' not found, using fallback model: ${fallback}`);
926
1119
  return returnWithTrace(configKey, sessionId, fallback, 0);
@@ -958,22 +1151,22 @@ async function get(configKey, sessionId, options = {}) {
958
1151
  const variantsRaw = config.variants;
959
1152
  const configVersion = config.version || targetVersion;
960
1153
  const variants = Array.isArray(variantsRaw) ? variantsRaw : Object.values(variantsRaw);
961
- log(`Config found! Version: ${configVersion}, Variants: ${JSON.stringify(variants)}`);
1154
+ log2(`Config found! Version: ${configVersion}, Variants: ${JSON.stringify(variants)}`);
962
1155
  const hashBytes = (0, import_crypto.createHash)("md5").update(sessionId).digest();
963
1156
  const hashVal = hashBytes.readUInt32BE(0) % 1e6;
964
- log(`Session hash: ${hashVal} (out of 1,000,000)`);
1157
+ log2(`Session hash: ${hashVal} (out of 1,000,000)`);
965
1158
  let cumulative = 0;
966
1159
  let assignedModel = variants[variants.length - 1].model;
967
1160
  for (const v of variants) {
968
1161
  const oldCumulative = cumulative;
969
1162
  cumulative += v.weight * 1e4;
970
- 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}`);
971
1164
  if (hashVal < cumulative) {
972
1165
  assignedModel = v.model;
973
1166
  break;
974
1167
  }
975
1168
  }
976
- log(`\u2705 Assigned model: ${assignedModel}`);
1169
+ log2(`\u2705 Assigned model: ${assignedModel}`);
977
1170
  return returnWithTrace(configKey, sessionId, assignedModel, configVersion);
978
1171
  } catch (e) {
979
1172
  if (e instanceof Error && e.message.includes("not found")) {
@@ -1022,12 +1215,13 @@ async function recordSession(configKey, version, sessionId, model) {
1022
1215
  }
1023
1216
 
1024
1217
  // src/init.ts
1025
- function init3(options = {}) {
1218
+ async function init3(options = {}) {
1026
1219
  const baseUrl3 = options.baseUrl || process.env.FALLOM_BASE_URL || "https://spans.fallom.com";
1027
- init({
1220
+ await init({
1028
1221
  apiKey: options.apiKey,
1029
1222
  baseUrl: baseUrl3,
1030
- captureContent: options.captureContent
1223
+ captureContent: options.captureContent,
1224
+ debug: options.debug
1031
1225
  });
1032
1226
  init2({
1033
1227
  apiKey: options.apiKey,