← back to Norma

agents/shared/audit-logger.js

122 lines

/**
 * Norma Audit Logger
 *
 * Logs every agent action to the sdcc_agent_actions table.
 * Provides a full audit trail of posts, replies, monitors, and errors.
 */

const { query } = require('./db');

/**
 * Log an agent action to the database.
 *
 * @param {Object} params
 * @param {string} params.agent - Agent name (e.g. 'reddit-agent', 'twitter-agent')
 * @param {string} params.actionType - Action type (e.g. 'post', 'reply', 'monitor', 'search')
 * @param {string} params.platform - Platform name (e.g. 'reddit', 'twitter', 'discord')
 * @param {string} [params.targetId] - Target identifier (post ID, thread ID, etc.)
 * @param {string} [params.targetUrl] - URL of the target content
 * @param {string} [params.content] - Content of the action (post body, reply text, etc.)
 * @param {string} [params.petitionId] - Associated petition UUID, if applicable
 * @param {string} [params.channel] - Channel or subreddit name
 * @param {Object} [params.responseData] - JSON response data from platform API
 * @param {string} [params.status='success'] - Action status: 'success', 'error', 'rate_limited', 'skipped'
 * @param {string} [params.errorMessage] - Error message if status is 'error'
 * @returns {Promise<Object>} The inserted row
 */
async function logAction({
  agent,
  actionType,
  platform,
  targetId = null,
  targetUrl = null,
  content = null,
  petitionId = null,
  channel = null,
  responseData = null,
  status = 'success',
  errorMessage = null,
}) {
  try {
    const result = await query(
      `INSERT INTO sdcc_agent_actions
        (agent, action_type, platform, target_id, target_url, content, petition_id, channel, response_data, status, error_message)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
       RETURNING *`,
      [
        agent,
        actionType,
        platform,
        targetId,
        targetUrl,
        content,
        petitionId,
        channel,
        responseData ? JSON.stringify(responseData) : null,
        status,
        errorMessage,
      ]
    );
    return result.rows[0];
  } catch (err) {
    // Don't let audit logging failures crash the agent
    console.error(`[${new Date().toISOString()}] [audit] Failed to log action:`, err.message);
    console.error(`[${new Date().toISOString()}] [audit] Action was: ${agent}/${actionType}/${platform}`);
    return null;
  }
}

/**
 * Get recent actions for an agent.
 *
 * @param {string} agent - Agent name to filter by
 * @param {number} [limit=50] - Maximum number of actions to return
 * @returns {Promise<Array>} Array of action rows, newest first
 */
async function getRecentActions(agent, limit = 50) {
  try {
    const { rows } = await query(
      `SELECT id, agent, action_type, platform, target_id, target_url,
              content, petition_id, channel, response_data, status,
              error_message, created_at
       FROM sdcc_agent_actions
       WHERE agent = $1
       ORDER BY created_at DESC
       LIMIT $2`,
      [agent, limit]
    );
    return rows;
  } catch (err) {
    console.error(`[${new Date().toISOString()}] [audit] Failed to get recent actions:`, err.message);
    return [];
  }
}

/**
 * Get action counts by type for an agent within a time window.
 * Useful for dashboards and status reports.
 *
 * @param {string} agent - Agent name
 * @param {number} [hoursBack=24] - How many hours back to count
 * @returns {Promise<Array>} Array of { action_type, platform, count, last_at }
 */
async function getActionSummary(agent, hoursBack = 24) {
  try {
    const { rows } = await query(
      `SELECT action_type, platform, status, COUNT(*)::int as count,
              MAX(created_at) as last_at
       FROM sdcc_agent_actions
       WHERE agent = $1 AND created_at > NOW() - INTERVAL '1 hour' * $2
       GROUP BY action_type, platform, status
       ORDER BY count DESC`,
      [agent, hoursBack]
    );
    return rows;
  } catch (err) {
    console.error(`[${new Date().toISOString()}] [audit] Failed to get action summary:`, err.message);
    return [];
  }
}

module.exports = { logAction, getRecentActions, getActionSummary };