Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preserve whitespaces for stacktraces #11

Open
thebiglabasky opened this issue Dec 11, 2024 · 5 comments
Open

Preserve whitespaces for stacktraces #11

thebiglabasky opened this issue Dec 11, 2024 · 5 comments
Labels
bug Something isn't working

Comments

@thebiglabasky
Copy link
Collaborator

It looks like we're not preserving whitespaces making it close to impossible to read the stacktrace:
Image

@thebiglabasky thebiglabasky added the bug Something isn't working label Dec 11, 2024
@thebiglabasky
Copy link
Collaborator Author

Note that it probably is a bug reporter issue since the JSON diagnostics contains the same thing, maybe worth checking the server logs response to ensure it remains formatted?

@filiphric
Copy link
Collaborator

I looked into this, but the json report is already formatted this way. I need to investigate on the metabase side and see how are error logs stored and see if it’s possible to maintain line breaks and other formatting

@thebiglabasky
Copy link
Collaborator Author

thebiglabasky commented Dec 12, 2024 via email

@thebiglabasky
Copy link
Collaborator Author

thebiglabasky commented Dec 17, 2024

I used Claude to the rescue and it gave me this code, which worked nicely to highlight Metabase frames from the server stacktraces, and offering to hide the default clojure ones.

import React, { useState } from 'react';

const StacktraceFormatter = () => {
  const [input, setInput] = useState('');
  const [showAllFrames, setShowAllFrames] = useState(false);

  const prettyPrintClojure = (str) => {
    let depth = 0;
    let inString = false;
    let formatted = '';
    
    for (let i = 0; i < str.length; i++) {
      const char = str[i];
      
      // Handle string literals
      if (char === '"' && str[i - 1] !== '\\') {
        inString = !inString;
        formatted += char;
        continue;
      }
      
      if (inString) {
        formatted += char;
        continue;
      }
      
      // Handle brackets and braces
      if (char === '{' || char === '[' || char === '(') {
        depth++;
        formatted += char + '\n' + '  '.repeat(depth);
      } else if (char === '}' || char === ']' || char === ')') {
        depth--;
        formatted += '\n' + '  '.repeat(depth) + char;
      } else if (char === ' ' && (str[i - 1] === ',' || str[i - 1] === '}' || str[i - 1] === ']' || str[i - 1] === ')')) {
        formatted += '\n' + '  '.repeat(depth);
      } else {
        formatted += char;
      }
    }
    return formatted;
  };

  const preprocessStacktrace = (trace) => {
    if (!trace) return '';
    return trace.replace(/\t+at\s+|\s+at\s+/g, '\n at ');
  };

  const formatStacktrace = (trace) => {
    if (!trace) return [];
    
    const preprocessed = preprocessStacktrace(trace);
    const lines = preprocessed.split('\n');
    const formattedLines = [];
    
    // Process first line (exception)
    if (lines[0]) {
      const match = lines[0].match(/^([^{[\n]+)({.+|[.+])/s);
      if (match) {
        const [_, prefix, clojureData] = match;
        formattedLines.push({
          type: 'exception',
          raw: lines[0].trim(),
          formatted: prefix + '\n' + prettyPrintClojure(clojureData)
        });
      } else {
        formattedLines.push({
          type: 'exception',
          raw: lines[0].trim(),
          formatted: lines[0].trim()
        });
      }
    }
    
    // Process stack frames
    const framePattern = /\s*at\s+([a-zA-Z0-9.$_/]+(?:\$[^(]+)?)\s*\(([\w.]+):(\d+)\)/;
    
    lines.slice(1).forEach(line => {
      const match = line.trim().match(framePattern);
      if (match) {
        const [_, fullMethod, file, lineNum] = match;
        const lastDotIndex = fullMethod.lastIndexOf('.');
        const namespace = fullMethod.substring(0, lastDotIndex);
        const method = fullMethod.substring(lastDotIndex + 1);
        
        const isMetabaseFrame = namespace.startsWith('metabase');
        
        if (showAllFrames || isMetabaseFrame) {
          formattedLines.push({
            type: 'frame',
            namespace,
            method,
            file,
            lineNum: parseInt(lineNum),
            raw: line.trim(),
            isMetabaseFrame
          });
        }
      }
    });
    
    return formattedLines;
  };

  const frames = formatStacktrace(input);
  const metabaseFrames = frames.filter(f => f.type === 'frame' && f.isMetabaseFrame);
  
  return (
    <div className="p-4 max-w-4xl">
      <textarea
        className="w-full p-2 border rounded mb-4 font-mono text-sm h-48"
        value={input}
        onChange={(e) => setInput(e.target.value)}
        placeholder="Paste Clojure stacktrace here..."
      />
      <div className="mb-4 flex items-center space-x-4">
        <label className="flex items-center space-x-2">
          <input
            type="checkbox"
            checked={showAllFrames}
            onChange={(e) => setShowAllFrames(e.target.checked)}
            className="rounded"
          />
          <span className="text-sm">Show all frames</span>
        </label>
        {metabaseFrames.length > 0 && (
          <span className="text-sm text-gray-600">
            {metabaseFrames.length} Metabase frames
          </span>
        )}
      </div>
      <div className="bg-gray-50 p-4 rounded border">
        {frames.map((line, i) => {
          if (line.type === 'exception') {
            return (
              <div key={i} className="font-mono text-sm mb-4 pl-4 border-l-4 border-red-500">
                <div className="text-gray-800 whitespace-pre">{line.formatted}</div>
              </div>
            );
          }
          return (
            <div 
              key={i} 
              className={`font-mono text-sm mb-1 ${line.isMetabaseFrame ? 'pl-4 border-l-4 border-yellow-500 bg-yellow-50' : ''}`}
            >
              <span className="text-gray-500">at </span>
              <span className={line.isMetabaseFrame ? 'text-blue-600' : 'text-gray-600'}>{line.namespace}</span>
              <span className="text-purple-600">.{line.method}</span>
              <span className="text-gray-600"> (</span>
              <span className="text-green-600">{line.file}:{line.lineNum}</span>
              <span className="text-gray-600">)</span>
            </div>
          );
        })}
      </div>
    </div>
  );
};

export default StacktraceFormatter;

@thebiglabasky
Copy link
Collaborator Author

This thing in action

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants