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
7 changes: 4 additions & 3 deletions __tests__/save.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ const SERVER_URL = process.env.SERVER_URL || 'http://localhost:38123';
const FIXTURES_DIR = path.resolve(import.meta.dir, '..', '.github/assets');
const tempFiles = [];

function tempPath() {
const p = path.join(os.tmpdir(), `oo-editors-test-${crypto.randomUUID()}.csv`);
function tempPath(ext) {
const p = path.join(os.tmpdir(), `oo-editors-test-${crypto.randomUUID()}${ext}`);
tempFiles.push(p);
return p;
}
Expand All @@ -31,6 +31,7 @@ afterAll(() => {

async function convertAndSave(fixtureName) {
const fixturePath = path.join(FIXTURES_DIR, fixtureName);
const fixtureExt = path.extname(fixtureName);

const convertRes = await fetch(
`${SERVER_URL}/api/convert?filepath=${encodeURIComponent(fixturePath)}`
Expand All @@ -43,7 +44,7 @@ async function convertAndSave(fixtureName) {
const binary = await convertRes.arrayBuffer();
expect(binary.byteLength).toBeGreaterThan(0);

const outPath = tempPath();
const outPath = tempPath(fixtureExt);
const saveRes = await fetch(
`${SERVER_URL}/api/save?filepath=${encodeURIComponent(outPath)}&filehash=${fileHash}`,
{
Expand Down
1 change: 1 addition & 0 deletions __tests__/server-utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ describe('getOutputFormatInfo', () => {
test('returns info for spreadsheet extensions', () => {
expect(getOutputFormatInfo('.xlsx')).toEqual({ code: 257, name: 'XLSX' });
expect(getOutputFormatInfo('.xls')).toEqual({ code: 257, name: 'XLSX' });
expect(getOutputFormatInfo('.xlsm')).toEqual({ code: 257, name: 'XLSX' });
expect(getOutputFormatInfo('.ods')).toEqual({ code: 257, name: 'XLSX' });
});

Expand Down
144 changes: 144 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"build:minify": "bunx esbuild server.js --minify --outfile=dist/server.js"
},
"dependencies": {
"@sentry/bun": "^10.20.0",
"@sentry/node": "^10.20.0",
"compression": "^1.8.1",
"express": "^4.18.2",
"xlsx": "^0.18.5"
Expand Down
259 changes: 259 additions & 0 deletions sentry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
const { version: OO_EDITORS_VERSION } = require('./package.json');

const OO_EDITOR_SENTRY_DSN = process.env.OO_EDITOR_SENTRY_DSN || '';
const DEFAULT_FLUSH_TIMEOUT_MS = 1500;
const BREADCRUMB_CUTOFF = 1024;
const STACK_CUTOFF = 4096;
const MAX_ARRAY_ITEMS = 20;
const MAX_OBJECT_KEYS = 20;
const MAX_DEPTH = 4;

const runtime = typeof Bun !== 'undefined' && typeof Bun.version === 'string'
? `bun-${Bun.version}`
: `node-${process.versions.node}`;

let loadAttempted = false;
let sentrySdk = null;
let initAttempted = false;
let enabled = false;

function truncate(value, maxLength) {
if (typeof value !== 'string' || value.length <= maxLength) {
return value;
}

return `${value.slice(0, maxLength - 3)}...`;
}

function sanitizeValue(value, depth = 0) {
if (value == null || typeof value === 'boolean' || typeof value === 'number') {
return value;
}

if (typeof value === 'string') {
return truncate(value, BREADCRUMB_CUTOFF);
}

if (value instanceof Error) {
return {
name: value.name,
message: truncate(value.message, BREADCRUMB_CUTOFF),
stack: truncate(value.stack || '', STACK_CUTOFF)
};
}

if (depth >= MAX_DEPTH) {
return Array.isArray(value) ? '[array]' : '[object]';
}

if (Array.isArray(value)) {
return value.slice(0, MAX_ARRAY_ITEMS).map((item) => sanitizeValue(item, depth + 1));
}

if (typeof value === 'object') {
const sanitized = {};
for (const key of Object.keys(value).slice(0, MAX_OBJECT_KEYS)) {
sanitized[key] = sanitizeValue(value[key], depth + 1);
}
return sanitized;
}

return truncate(String(value), BREADCRUMB_CUTOFF);
}

function loadSentrySdk() {
if (loadAttempted) {
return sentrySdk;
}

loadAttempted = true;
const packageName = typeof Bun !== 'undefined' && typeof Bun.version === 'string'
? '@sentry/bun'
: '@sentry/node';

try {
sentrySdk = require(packageName);
} catch (error) {
console.error(`[oo-editors:SENTRY] failed to load ${packageName}:`, error);
sentrySdk = null;
}

return sentrySdk;
}

function applyScopeContext(scope, context = {}) {
if (context.level) {
scope.setLevel(context.level);
}

if (context.tags) {
for (const [key, value] of Object.entries(context.tags)) {
if (value != null) {
scope.setTag(key, String(value));
}
}
}

if (context.fingerprint) {
scope.setFingerprint(context.fingerprint.map((part) => String(part)));
}

if (context.data) {
scope.setContext('oo_editors', sanitizeValue(context.data));
}
}

function initOoEditorsSentry() {
if (initAttempted) {
return enabled;
}

initAttempted = true;
if (!OO_EDITOR_SENTRY_DSN) {
return false;
}

const Sentry = loadSentrySdk();
if (!Sentry) {
return false;
}

try {
Sentry.init({
dsn: OO_EDITOR_SENTRY_DSN,
release: `oo-editors@${OO_EDITORS_VERSION}`,
environment: process.env.NODE_ENV || 'development',
defaultIntegrations: false,
disableInstrumentationWarnings: true,
maxBreadcrumbs: 100,
shutdownTimeout: DEFAULT_FLUSH_TIMEOUT_MS,
beforeBreadcrumb(breadcrumb) {
if (!breadcrumb) {
return null;
}

return {
...breadcrumb,
data: sanitizeValue(breadcrumb.data || {})
};
},
initialScope(scope) {
scope.setTag('service', 'oo-editors');
scope.setTag('runtime', runtime);
scope.setTag('version', OO_EDITORS_VERSION);
return scope;
}
});

enabled = true;
addLifecycleBreadcrumb('sentry initialized', {
runtime,
release: `oo-editors@${OO_EDITORS_VERSION}`
});
console.log('[oo-editors:SENTRY] initialized');
return true;
} catch (error) {
console.error('[oo-editors:SENTRY] init failed:', error);
enabled = false;
return false;
}
}

function addLifecycleBreadcrumb(message, data = {}, options = {}) {
if (!enabled) {
return false;
}

const Sentry = loadSentrySdk();
if (!Sentry) {
return false;
}

Sentry.addBreadcrumb({
category: options.category || 'oo-editors.lifecycle',
type: options.type || 'default',
level: options.level || 'info',
message,
data: sanitizeValue(data)
});

return true;
}

function captureLifecycleMessage(message, context = {}) {
if (!enabled) {
return false;
}

const Sentry = loadSentrySdk();
if (!Sentry) {
return false;
}

Sentry.withScope((scope) => {
applyScopeContext(scope, context);
Sentry.captureMessage(message);
});

return true;
}

function captureLifecycleException(error, context = {}) {
if (!enabled) {
return false;
}

const Sentry = loadSentrySdk();
if (!Sentry) {
return false;
}

Sentry.withScope((scope) => {
const normalizedError = error instanceof Error ? error : new Error(String(error));
const details = error instanceof Error
? context.data
: {
rawError: sanitizeValue(error),
...(context.data || {})
};

applyScopeContext(scope, {
...context,
data: details
});
Sentry.captureException(normalizedError);
});

return true;
}

async function flushSentry(timeoutMs = DEFAULT_FLUSH_TIMEOUT_MS) {
if (!enabled) {
return true;
}

const Sentry = loadSentrySdk();
if (!Sentry) {
return false;
}

try {
return await Sentry.flush(timeoutMs);
} catch (error) {
console.error('[oo-editors:SENTRY] flush failed:', error);
return false;
}
}

function isSentryEnabled() {
return enabled;
}

module.exports = {
initOoEditorsSentry,
isSentryEnabled,
addLifecycleBreadcrumb,
captureLifecycleMessage,
captureLifecycleException,
flushSentry
};
2 changes: 1 addition & 1 deletion server-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function getX2TFormatCode(formatString) {
function getOutputFormatInfo(ext) {
const normalized = ext.toLowerCase();

if (normalized === '.xlsx' || normalized === '.xls' || normalized === '.ods') {
if (normalized === '.xlsx' || normalized === '.xls' || normalized === '.xlsm' || normalized === '.ods') {
return { code: 257, name: 'XLSX' };
} else if (normalized === '.csv') {
return { code: 260, name: 'CSV' };
Expand Down
Loading
Loading