Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 30 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
"multer": "^1.4.3",
"nice-cache": "^0.0.5",
"oc-client": "^4.0.2",
"oc-client-browser": "^2.1.4",
"oc-client-browser": "^2.1.5",
"oc-empty-response-handler": "^1.0.2",
"oc-get-unix-utc-timestamp": "^1.0.6",
"oc-s3-storage-adapter": "^2.2.0",
Expand All @@ -117,6 +117,7 @@
"semver": "^7.7.1",
"semver-extra": "^3.0.0",
"serialize-error": "^8.1.0",
"source-map": "^0.7.6",
"targz": "^1.0.1",
"try-require": "^1.2.1",
"undici": "^6.21.1",
Expand Down
206 changes: 206 additions & 0 deletions src/registry/routes/helpers/format-error-stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// import fs from 'fs';
import path from 'node:path';
import source from 'source-map';

function extractInlineSourceMap(code: string) {
try {
const map = code.match(
/\/\/# sourceMappingURL=data:application\/json;charset=utf-8;base64,(.*)/
)?.[1];
if (map) {
return atob(map);
}
return null;
} catch {
return null;
}
}

export async function processStackTrace({
stackTrace,
code
}: {
stackTrace: string;
code: string;
}) {
const rawSourceMap = extractInlineSourceMap(code);
if (!rawSourceMap) return null;
const consumer = await new source.SourceMapConsumer(rawSourceMap);
const lines = stackTrace.split('\n').filter((l) => l.trim().startsWith('at'));

const result = {
stack: [] as string[],
codeFrame: [] as string[]
};

for (const line of lines) {
// More flexible regex to handle different stack trace formats
let match = line.match(/at (.+) \((.+):(\d+):(\d+)\)/);
if (!match) {
// Handle lines without function names like "at /path/file.js:line:col"
match = line.match(/at (.+):(\d+):(\d+)/);
if (match) {
const [, file, lineStr, colStr] = match;
match = [null, null, file, lineStr, colStr] as any;
}
}

if (!match) {
result.stack.push(`${line.trim()} (could not parse)`);
continue;
}

const [, functionName, file, lineStr, colStr] = match;
const lineNum = parseInt(lineStr, 10);
const colNum = parseInt(colStr, 10);

// Check if this line is from the file we have a source map for
if (!file.includes('server.js')) {
result.stack.push(`${line.trim()} (no source map)`);
continue;
}

const original = consumer.originalPositionFor({
line: lineNum,
column: colNum
});

if (original.source && original.line !== null) {
// Filter out frames that map to external libraries or oc-server internals AFTER mapping
if (
original.source.includes('node_modules') ||
original.source.includes('oc-server') ||
original.source.includes('__oc_higherOrderServer')
) {
// Don't show filtered frames
continue;
}

// Try to get the function name from multiple sources
let displayName = original.name || functionName || '<anonymous>';

// Clean up the function name if it includes object/class info
if (functionName && !original.name) {
displayName = functionName;
}

// Make file paths relative to current directory for better readability
let relativePath = original.source;
try {
if (path.isAbsolute(original.source)) {
relativePath = path.relative(process.cwd(), original.source);
}
} catch {
// Keep original path if relative conversion fails
}

const stackLine = `at ${displayName} (${relativePath}:${original.line}:${original.column})`;
result.stack.push(stackLine);

// Show source code context if available
const sourceContent = consumer.sourceContentFor(original.source, true);
if (sourceContent && original.line) {
const codeFrame = getCodeFrame(
sourceContent,
original.line,
original.column || 0
);
if (codeFrame) {
result.codeFrame.push(codeFrame);
}
}
} else {
// Fallback to original line if source mapping fails
result.stack.push(`${line.trim()} (source map failed)`);
}
}

// Don't forget to destroy the consumer
try {
consumer.destroy();
} catch {}

return {
stack: result.stack.join('\n'),
frame: [
// For some reason, the first block lacks some indentation
...result.codeFrame.slice(0, 1).map((x) => ` ${x}`),
...result.codeFrame.slice(1)
].join('\n')
};
}

// Helper function to show code context around the error
function getCodeFrame(
sourceContent: string,
line: number,
column: number,
contextLines = 2
) {
try {
const lines = sourceContent.split('\n');
const targetLine = line - 1; // Convert to 0-based

if (targetLine < 0 || targetLine >= lines.length) {
return null;
}

const start = Math.max(0, targetLine - contextLines);
const end = Math.min(lines.length, targetLine + contextLines + 1);

let result = '';
for (let i = start; i < end; i++) {
const lineNumber = i + 1;
const isTarget = i === targetLine;
const prefix = isTarget ? '> ' : ' ';
const lineNumberStr = String(lineNumber).padStart(3, ' ');

result += `${prefix}${lineNumberStr} | ${lines[i]}\n`;

// Add pointer to exact column for target line
if (isTarget && column > 0) {
const pointer = ' '.repeat(column); // Account for prefix and line number
result += `${' '.repeat(5)} | ${pointer}^\n`;
}
}

return result.trim();
} catch {
return null;
}
}

// async function main() {
// const stackTrace = `
// TypeError: Cannot read properties of undefined (reading 'name')
// at HandledServer.initial (/Users/ricardo.agullo/Dev/octests/helpai/_package/server.js:196:38)
// at async ocServerWrapper (/Users/ricardo.agullo/Dev/octests/helpai/_package/server.js:83:19)
// `;
// const rawSourceMap = fs.readFileSync(
// './helpai/_package/server.js.map',
// 'utf8'
// );
// const { stack, codeFrame } = await processStackTrace({
// stackTrace,
// rawSourceMap
// });

// // Log the stack trace
// for (const line of stack) {
// console.log(` ${line}`);
// }

// // Log the code frames
// // for (const frame of codeFrame) {
// // console.log(`\n${frame}\n`);
// // }
// for (let i = 0; i < codeFrame.length; i++) {
// if (i === 0) {
// console.log(`\n ${codeFrame[i]}\n`);
// } else {
// console.log(`\n${codeFrame[i]}\n`);
// }
// }
// }

// main().catch(console.error);
30 changes: 25 additions & 5 deletions src/registry/routes/helpers/get-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import * as urlBuilder from '../../domain/url-builder';
import * as validator from '../../domain/validators';
import { validateTemplateOcVersion } from '../../domain/validators';
import applyDefaultValues from './apply-default-values';
import { processStackTrace } from './format-error-stack';
import * as getComponentFallback from './get-component-fallback';
import GetComponentRetrievingInfo from './get-component-retrieving-info';

Expand Down Expand Up @@ -136,7 +137,7 @@ export default function getComponent(conf: Config, repository: Repository) {
return env;
};

const renderer = (
const renderer = async (
options: RendererOptions,
cb: (result: GetComponentResult) => void
) => {
Expand Down Expand Up @@ -311,7 +312,7 @@ export default function getComponent(conf: Config, repository: Repository) {
);
};

const returnComponent = (err: any, data: any) => {
const returnComponent = async (err: any, data: any) => {
if (componentCallbackDone) {
return;
}
Expand Down Expand Up @@ -381,7 +382,7 @@ export default function getComponent(conf: Config, repository: Repository) {
error: err
});

return callback({
const response = {
status,
response: {
code: 'GENERIC_ERROR',
Expand All @@ -391,10 +392,29 @@ export default function getComponent(conf: Config, repository: Repository) {
details: {
message: err.message,
stack: err.stack,
originalError: err
originalError: err,
frame: undefined as string | undefined
}
}
});
};

if (conf.local && err.stack) {
const { content } = await repository
.getDataProvider(component.name, component.version)
.catch(() => ({ content: null }));
if (content) {
const processedStack = await processStackTrace({
stackTrace: err.stack,
code: content
}).catch(() => null);
if (processedStack) {
response.response.details.stack = processedStack.stack;
response.response.details.frame = processedStack.frame;
}
}
}

return callback(response);
}

const response: {
Expand Down
Loading