Next.js website for Rocky Mountain Vending company featuring: - Product catalog with Stripe integration - Service areas and parts pages - Admin dashboard with Clerk authentication - SEO optimized pages with JSON-LD structured data Co-authored-by: Cursor <cursoragent@cursor.com>
255 lines
8.4 KiB
Text
255 lines
8.4 KiB
Text
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
|
|
const currentScopes = require('../../currentScopes.js');
|
|
const exports$1 = require('../../exports.js');
|
|
const trace = require('../../tracing/trace.js');
|
|
const genAiAttributes = require('../gen-ai-attributes.js');
|
|
const constants = require('./constants.js');
|
|
const utils = require('./utils.js');
|
|
|
|
/**
|
|
* Extract request attributes from method arguments
|
|
*/
|
|
function extractRequestAttributes(args, methodPath) {
|
|
const attributes = {
|
|
[genAiAttributes.GEN_AI_SYSTEM_ATTRIBUTE]: 'openai',
|
|
[genAiAttributes.GEN_AI_OPERATION_NAME_ATTRIBUTE]: utils.getOperationName(methodPath),
|
|
};
|
|
|
|
if (args.length > 0 && typeof args[0] === 'object' && args[0] !== null) {
|
|
const params = args[0] ;
|
|
|
|
attributes[genAiAttributes.GEN_AI_REQUEST_MODEL_ATTRIBUTE] = params.model ?? 'unknown';
|
|
if ('temperature' in params) attributes[genAiAttributes.GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE] = params.temperature;
|
|
if ('top_p' in params) attributes[genAiAttributes.GEN_AI_REQUEST_TOP_P_ATTRIBUTE] = params.top_p;
|
|
if ('frequency_penalty' in params)
|
|
attributes[genAiAttributes.GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE] = params.frequency_penalty;
|
|
if ('presence_penalty' in params) attributes[genAiAttributes.GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE] = params.presence_penalty;
|
|
} else {
|
|
attributes[genAiAttributes.GEN_AI_REQUEST_MODEL_ATTRIBUTE] = 'unknown';
|
|
}
|
|
|
|
return attributes;
|
|
}
|
|
|
|
/**
|
|
* Helper function to set token usage attributes
|
|
*/
|
|
function setTokenUsageAttributes(
|
|
span,
|
|
promptTokens,
|
|
completionTokens,
|
|
totalTokens,
|
|
) {
|
|
if (promptTokens !== undefined) {
|
|
span.setAttributes({
|
|
[genAiAttributes.OPENAI_USAGE_PROMPT_TOKENS_ATTRIBUTE]: promptTokens,
|
|
[genAiAttributes.GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: promptTokens,
|
|
});
|
|
}
|
|
if (completionTokens !== undefined) {
|
|
span.setAttributes({
|
|
[genAiAttributes.OPENAI_USAGE_COMPLETION_TOKENS_ATTRIBUTE]: completionTokens,
|
|
[genAiAttributes.GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: completionTokens,
|
|
});
|
|
}
|
|
if (totalTokens !== undefined) {
|
|
span.setAttributes({
|
|
[genAiAttributes.GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: totalTokens,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function to set common response attributes (ID, model, timestamp)
|
|
*/
|
|
function setCommonResponseAttributes(span, id, model, timestamp) {
|
|
if (id) {
|
|
span.setAttributes({
|
|
[genAiAttributes.OPENAI_RESPONSE_ID_ATTRIBUTE]: id,
|
|
[genAiAttributes.GEN_AI_RESPONSE_ID_ATTRIBUTE]: id,
|
|
});
|
|
}
|
|
if (model) {
|
|
span.setAttributes({
|
|
[genAiAttributes.OPENAI_RESPONSE_MODEL_ATTRIBUTE]: model,
|
|
[genAiAttributes.GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: model,
|
|
});
|
|
}
|
|
if (timestamp) {
|
|
span.setAttributes({
|
|
[genAiAttributes.OPENAI_RESPONSE_TIMESTAMP_ATTRIBUTE]: new Date(timestamp * 1000).toISOString(),
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add attributes for Chat Completion responses
|
|
*/
|
|
function addChatCompletionAttributes(span, response) {
|
|
setCommonResponseAttributes(span, response.id, response.model, response.created);
|
|
if (response.usage) {
|
|
setTokenUsageAttributes(
|
|
span,
|
|
response.usage.prompt_tokens,
|
|
response.usage.completion_tokens,
|
|
response.usage.total_tokens,
|
|
);
|
|
}
|
|
if (Array.isArray(response.choices)) {
|
|
const finishReasons = response.choices
|
|
.map(choice => choice.finish_reason)
|
|
.filter((reason) => reason !== null);
|
|
if (finishReasons.length > 0) {
|
|
span.setAttributes({
|
|
[genAiAttributes.GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: JSON.stringify(finishReasons),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add attributes for Responses API responses
|
|
*/
|
|
function addResponsesApiAttributes(span, response) {
|
|
setCommonResponseAttributes(span, response.id, response.model, response.created_at);
|
|
if (response.status) {
|
|
span.setAttributes({
|
|
[genAiAttributes.GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: JSON.stringify([response.status]),
|
|
});
|
|
}
|
|
if (response.usage) {
|
|
setTokenUsageAttributes(
|
|
span,
|
|
response.usage.input_tokens,
|
|
response.usage.output_tokens,
|
|
response.usage.total_tokens,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add response attributes to spans
|
|
* This currently supports both Chat Completion and Responses API responses
|
|
*/
|
|
function addResponseAttributes(span, result, recordOutputs) {
|
|
if (!result || typeof result !== 'object') return;
|
|
|
|
const response = result ;
|
|
|
|
if (utils.isChatCompletionResponse(response)) {
|
|
addChatCompletionAttributes(span, response);
|
|
if (recordOutputs && response.choices?.length) {
|
|
const responseTexts = response.choices.map(choice => choice.message?.content || '');
|
|
span.setAttributes({ [genAiAttributes.GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: JSON.stringify(responseTexts) });
|
|
}
|
|
} else if (utils.isResponsesApiResponse(response)) {
|
|
addResponsesApiAttributes(span, response);
|
|
if (recordOutputs && response.output_text) {
|
|
span.setAttributes({ [genAiAttributes.GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: response.output_text });
|
|
}
|
|
}
|
|
}
|
|
|
|
// Extract and record AI request inputs, if present. This is intentionally separate from response attributes.
|
|
function addRequestAttributes(span, params) {
|
|
if ('messages' in params) {
|
|
span.setAttributes({ [genAiAttributes.GEN_AI_REQUEST_MESSAGES_ATTRIBUTE]: JSON.stringify(params.messages) });
|
|
}
|
|
if ('input' in params) {
|
|
span.setAttributes({ [genAiAttributes.GEN_AI_REQUEST_MESSAGES_ATTRIBUTE]: JSON.stringify(params.input) });
|
|
}
|
|
}
|
|
|
|
function getOptionsFromIntegration() {
|
|
const scope = currentScopes.getCurrentScope();
|
|
const client = scope.getClient();
|
|
const integration = client?.getIntegrationByName(constants.OPENAI_INTEGRATION_NAME) ;
|
|
const shouldRecordInputsAndOutputs = integration ? Boolean(client?.getOptions().sendDefaultPii) : false;
|
|
|
|
return {
|
|
recordInputs: integration?.options?.recordInputs ?? shouldRecordInputsAndOutputs,
|
|
recordOutputs: integration?.options?.recordOutputs ?? shouldRecordInputsAndOutputs,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Instrument a method with Sentry spans
|
|
* Following Sentry AI Agents Manual Instrumentation conventions
|
|
* @see https://docs.sentry.io/platforms/javascript/guides/node/tracing/instrumentation/ai-agents-module/#manual-instrumentation
|
|
*/
|
|
function instrumentMethod(
|
|
originalMethod,
|
|
methodPath,
|
|
context,
|
|
options,
|
|
) {
|
|
return async function instrumentedMethod(...args) {
|
|
const finalOptions = options || getOptionsFromIntegration();
|
|
const requestAttributes = extractRequestAttributes(args, methodPath);
|
|
const model = (requestAttributes[genAiAttributes.GEN_AI_REQUEST_MODEL_ATTRIBUTE] ) || 'unknown';
|
|
const operationName = utils.getOperationName(methodPath);
|
|
|
|
return trace.startSpan(
|
|
{
|
|
name: `${operationName} ${model}`,
|
|
op: utils.getSpanOperation(methodPath),
|
|
attributes: requestAttributes ,
|
|
},
|
|
async (span) => {
|
|
try {
|
|
if (finalOptions.recordInputs && args[0] && typeof args[0] === 'object') {
|
|
addRequestAttributes(span, args[0] );
|
|
}
|
|
|
|
const result = await originalMethod.apply(context, args);
|
|
// TODO: Add streaming support
|
|
addResponseAttributes(span, result, finalOptions.recordOutputs);
|
|
return result;
|
|
} catch (error) {
|
|
exports$1.captureException(error);
|
|
throw error;
|
|
}
|
|
},
|
|
);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a deep proxy for OpenAI client instrumentation
|
|
*/
|
|
function createDeepProxy(target, currentPath = '', options) {
|
|
return new Proxy(target, {
|
|
get(obj, prop) {
|
|
const value = (obj )[prop];
|
|
const methodPath = utils.buildMethodPath(currentPath, String(prop));
|
|
|
|
if (typeof value === 'function' && utils.shouldInstrument(methodPath)) {
|
|
return instrumentMethod(value , methodPath, obj, options);
|
|
}
|
|
|
|
if (typeof value === 'function') {
|
|
// Bind non-instrumented functions to preserve the original `this` context,
|
|
// which is required for accessing private class fields (e.g. #baseURL) in OpenAI SDK v5.
|
|
return value.bind(obj);
|
|
}
|
|
|
|
if (value && typeof value === 'object') {
|
|
return createDeepProxy(value , methodPath, options);
|
|
}
|
|
|
|
return value;
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Instrument an OpenAI client with Sentry tracing
|
|
* Can be used across Node.js, Cloudflare Workers, and Vercel Edge
|
|
*/
|
|
function instrumentOpenAiClient(client, options) {
|
|
return createDeepProxy(client, '', options);
|
|
}
|
|
|
|
exports.instrumentOpenAiClient = instrumentOpenAiClient;
|
|
//# sourceMappingURL=index.js.map
|