[JS] Telemetry Not Exported When Flow is Called from Cloud Function
Describe the bug Telemetry data is not being exported when a flow is executed from a Firebase Cloud Function. However, traces appear correctly when the same flow is run from the Genkit UI in Firebase Console.
To Reproduce
- Enable Firebase telemetry with dev export:
import { enableFirebaseTelemetry } from "@genkit-ai/firebase";
enableFirebaseTelemetry({
forceDevExport: true
})
export const ai = genkit({
...
});
- Define a flow using
defineFlow():
export const chatFlow = ai.defineFlow(
{
name: "chatFlow",
inputSchema: InputSchemaZod,
outputSchema: OutputSchemaZod,
},
async (input) => {
...
}
);
- Running the flow from Genkit UI → Traces appear correctly in Firebase Console / Genkit
- Running the flow from Cloud Function → No traces appear in Firebase Console / Genkit.
Expected behavior
Traces should be visible in Firebase Console / Genkit when the flow is executed from a Cloud Function.
Runtime (please complete the following information):
- OS: MacOS
- Version 15.3.1
Node version
v22.12.0
cc: @trevor-rex
I think I've found what is going on: We use Sentry, here is a conflict in OpenTelemetry configuration between Sentry and Firebase Telemetry.
"Happy" to see that i'm not the only one. Now the question is : is a workaround or fix possible ?
I am not sure that this is a valid solution, but I am using manual instrumentation using newrelic:
- newRelic.startSegment() to trace performance
- newRelic.recordCustomEvent() to track token usage and other metrics
@mike-4040 Hey! Where does the newRelic come from? Could you share your workaround's code please?
My setup is very simple:
-
NewRelicManager
/**
* NewRelicManager is a singleton class that manages the New Relic instance.
* It ensures that the New Relic instance is only created once and provides a
* convenient way to access it using a getter.
*/
export class NewRelicManager {
private static _newRelic: NewRelic | undefined;
private constructor() {
// Private constructor to prevent instantiation
}
public static get newRelic(): NewRelic | undefined {
if (!process.env.NEW_RELIC_APP_NAME) {
errorHandler(
new InternalError("NewRelicManager-NEW_RELIC_APP_NAME-notSet")
);
return undefined;
}
if (!process.env.NEW_RELIC_LICENSE_KEY) {
errorHandler(new InternalError("newRelicManager-licenseKey-notSet"));
return undefined;
}
if (!NewRelicManager._newRelic) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
NewRelicManager._newRelic = require("newrelic");
}
return NewRelicManager._newRelic;
}
}
-
nrRecordAIInvocation()helper
/**
* Represents an AI invocation event recorded in New Relic.
*
* - Additional attributes must be **string**, **number**, or **boolean**.
* - Key names **must be shorter than 255 characters**.
*/
interface NREventAIInvocation {
inputTokens: number;
modelName: string;
outputTokens: number;
taskName: FlowName;
[key: string]: string | number | boolean;
}
/**
* Records an AI invocation event as a custom event in New Relic.
*
* - If **New Relic is not enabled**, this function will do nothing.
* - The event includes standard fields (`inputTokens`, `outputTokens`, etc.)
* along with any additional attributes.
*/
export function nrRecordAIInvocation(attributes: NREventAIInvocation) {
const { newRelic } = NewRelicManager;
if (!newRelic) {
return;
}
newRelic.recordCustomEvent(NRCustomEventType.AI_INVOCATION, attributes);
}
-
nrStartSegment()helper
/**
* Starts a New Relic segment `name` and executes the `fn()` with the given `input`.
*
* - If **New Relic is not enabled**, the `fn()` will execute directly.
* - If **New Relic is enabled**, the `fn()` will execute within a segment.
* - The `fn()` should accept a single input parameter, for simplicity.
*
* @param name - The name for the segment.
* @param fn - The function to execute within the segment.
* @param input - The input to pass to the function
* @returns The result of the function execution.
*
* @example
* ```typescript
* const result = await nrStartSegment(
* "mySegmentName",
* myAsyncFunction,
* myInput
* );
* ```
* @example
* If you need to invoke a object method, you can use the following pattern:
* ```typescript
* const result = await nrStartSegment(
* "myObject.someMethod",
* (input) => myObject.someMethod(input),
* myInput
* );
* ```
*/
export async function nrStartSegment<TI, TO>(
name: string,
fn: (input: TI) => Promise<TO>, // function to execute
input: TI
): Promise<TO> {
const { newRelic } = NewRelicManager;
if (!newRelic) {
return fn(input);
}
return newRelic.startSegment(name, true, () => fn(input));
}
And then use them:
// track flow performance
const summary = await nrStartSegment(
FLOW_NAMES.chatSummaryFlow, // just a name
chatSummaryFlow, // flow
{ messages } // flow input/argiments
);
// Inside the flow
// track token usage
const { output, usage } = await ai.generate({
model,
prompt,
output: {
schema: OutputSchema,
},
});
nrRecordAIInvocation({
inputTokens: usage.inputTokens || 0,
modelName: model.name,
outputTokens: usage.outputTokens || 0,
taskName: FLOW_NAMES.chatSummaryFlow,
});
We use newrelic for here because we like it better then sentry for performance monitoring, but sentry have a very similar functionality, so you can use manual/custom instrumentation from sentry.
@mike-4040 Thank you for sharing the code
For people using Sentry and Genkit and want traces exported to Genkit without disabling Sentry, I tried this and it seems to work for me: https://github.com/firebase/genkit/issues/2904#issuecomment-3509943920