PhoenixKit.Emails.Log (phoenix_kit v1.6.4)

View Source

Email logging for PhoenixKit - comprehensive logging in a single module.

This module provides both the Ecto schema definition and business logic for managing emails. It includes email creation tracking, status updates, event relationships, and analytics functions.

Schema Fields

  • message_id: Internal unique identifier (pk_XXXXX format) generated before sending (required, unique)
  • aws_message_id: AWS SES message ID from provider response (optional, unique when present)
  • to: Recipient email address (required)
  • from: Sender email address (required)
  • subject: Email subject line
  • headers: JSONB map of email headers (without duplication)
  • body_preview: Preview of email content (first 500+ characters)
  • body_full: Complete email body content (optional, settings-controlled)
  • template_name: Name/identifier of email template used
  • campaign_id: Campaign or group identifier for analytics
  • attachments_count: Number of email attachments
  • size_bytes: Total email size in bytes
  • retry_count: Number of send retry attempts
  • error_message: Error message if sending failed
  • status: Current status (queued, sent, delivered, bounced, opened, clicked, failed, etc.)
  • queued_at: Timestamp when email was queued for sending
  • sent_at: Timestamp when email was sent
  • delivered_at: Timestamp when email was delivered (from provider)
  • rejected_at: Timestamp when email was rejected by provider
  • failed_at: Timestamp when email send failed
  • delayed_at: Timestamp when email delivery was delayed
  • bounced_at: Timestamp when email bounced
  • complained_at: Timestamp when spam complaint was received
  • opened_at: Timestamp when email was first opened
  • clicked_at: Timestamp when first link was clicked
  • configuration_set: AWS SES configuration set used
  • message_tags: JSONB tags for grouping and analytics
  • provider: Email provider used (aws_ses, smtp, local, etc.)
  • user_id: Associated user ID for authentication emails

Message ID Strategy

PhoenixKit uses a dual message ID strategy to handle the lifecycle of email tracking:

1. Internal Message ID (message_id)

  • Format: pk_XXXXX (PhoenixKit prefix + random hex)
  • Generated: BEFORE email is sent (in EmailInterceptor)
  • Purpose: Primary identifier for database operations
  • Uniqueness: Always unique, never null
  • Usage: Used in logs, events, and internal correlation

2. AWS SES Message ID (aws_message_id)

  • Format: Provider-specific (e.g., AWS SES format)
  • Generated: AFTER email is sent (from provider response)
  • Purpose: Correlation with AWS SES events (SNS/SQS)
  • Uniqueness: Unique when present, nullable
  • Usage: Used to match SQS events to email logs

Workflow

1. Email Created
   > EmailInterceptor generates message_id (pk_12345)
   > EmailLog created with message_id = "pk_12345"
   > aws_message_id = nil (not yet sent)

2. Email Sent via AWS SES
   > AWS returns MessageId = "0102abc-def-ghi"
   > EmailInterceptor updates:
       - message_id stays "pk_12345" (unchanged)
       - aws_message_id = "0102abc-def-ghi"
       - message_tags stores both for debugging

3. SQS Event Received
   > Event contains AWS MessageId = "0102abc-def-ghi"
   > SQSProcessor searches:
       a) First by message_id (if starts with pk_)
       b) Then by aws_message_id field
       c) Then in headers/metadata
   > Updates EmailLog and creates EmailEvent

Benefits of Dual Strategy

  • Early Tracking: Can create logs before provider response
  • Event Correlation: AWS message_id links to SQS events
  • Robustness: Multiple search strategies prevent missed events
  • Debugging: Both IDs stored in message_tags for troubleshooting
  • No Duplication: Partial unique index prevents duplicate aws_message_id

Search Priority in SQSProcessor

# 1. Direct message_id search (for internal IDs)
get_log_by_message_id(message_id)

# 2. AWS message_id field search (for provider IDs)
find_by_aws_message_id(aws_message_id)

# 3. Metadata search (fallback for legacy data)
# searches in headers for aws_message_id

Database Constraints

  • message_id: UNIQUE NOT NULL
  • aws_message_id: PARTIAL UNIQUE (WHERE aws_message_id IS NOT NULL)
  • Composite index: (message_id, aws_message_id) for fast correlation

Core Functions

Email Log Management

Status Management

Analytics Functions

System Functions

Usage Examples

# Create a new email log
{:ok, log} = PhoenixKit.Emails.Log.create_log(%{
  message_id: "msg-abc123",
  to: "user@example.com",
  from: "noreply@myapp.com",
  subject: "Welcome to MyApp",
  template_name: "welcome_email",
  campaign_id: "welcome_series",
  provider: "aws_ses"
})

# Update status when delivered
{:ok, updated_log} = PhoenixKit.Emails.Log.mark_as_delivered(
  log, DateTime.utc_now()
)

# Get campaign statistics
stats = PhoenixKit.Emails.Log.get_campaign_stats("newsletter_2024")

Summary

Functions

Returns an %Ecto.Changeset{} for tracking email log changes.

Creates a changeset for email log creation and updates.

Removes emails older than specified number of days.

Compresses body_full field for logs older than specified days. Sets body_full to nil to save storage space while keeping body_preview.

Counts emails with optional filtering (without loading all records).

Creates an email log from a Swoosh.Email struct.

Creates an email log.

Deletes an email log.

Finds an email log by AWS message ID.

Gets statistics for a specific campaign.

Gets daily delivery trend data for charts.

Gets engagement metrics for analysis.

Gets a single email log by ID.

Gets a single email log by message ID from the email provider.

Gets logs ready for archival to external storage.

Gets provider-specific performance metrics.

Gets statistics for a specific time period.

Returns a list of emails with optional filters.

Marks an email as bounced with type and reason.

Marks an email as clicked with link URL and timestamp.

Marks an email as delayed with delay information.

Marks an email as delivered with timestamp.

Marks an email as failed with error reason.

Marks an email as opened with timestamp.

Marks an email as queued with timestamp.

Marks an email as rejected by provider with reason.

Marks an email as sent with timestamp.

Updates an email log.

Updates the status of an email log.

Validates email format using basic regex pattern.

Functions

change_log(email_log, attrs \\ %{})

Returns an %Ecto.Changeset{} for tracking email log changes.

Examples

iex> PhoenixKit.Emails.Log.change_log(log)
%Ecto.Changeset{data: %PhoenixKit.Emails.Log{}}

changeset(email_log, attrs)

Creates a changeset for email log creation and updates.

Validates required fields and ensures data consistency. Automatically sets sent_at on new records if not provided.

cleanup_old_logs(days_old \\ 90)

Removes emails older than specified number of days.

Examples

iex> PhoenixKit.Emails.Log.cleanup_old_logs(90)
{5, nil}  # Deleted 5 records

compress_old_bodies(days_old \\ 30)

Compresses body_full field for logs older than specified days. Sets body_full to nil to save storage space while keeping body_preview.

Examples

iex> PhoenixKit.Emails.Log.compress_old_bodies(30)
{12, nil}  # Compressed 12 records

count_logs(filters \\ %{})

Counts emails with optional filtering (without loading all records).

Parameters

  • filters - Map of filters to apply (optional)

Examples

iex> PhoenixKit.Emails.Log.count_logs(%{status: "bounced"})
42

create_from_swoosh_email(email, opts \\ [])

Creates an email log from a Swoosh.Email struct.

Extracts relevant data and creates an appropriately formatted log entry.

Examples

iex> email = new() |> to("user@example.com") |> from("app@example.com")
iex> PhoenixKit.Emails.Log.create_from_swoosh_email(email, provider: "aws_ses")
{:ok, %PhoenixKit.Emails.Log{}}

create_log(attrs \\ %{})

Creates an email log.

Examples

iex> PhoenixKit.Emails.Log.create_log(%{message_id: "abc", to: "user@test.com"})
{:ok, %PhoenixKit.Emails.Log{}}

iex> PhoenixKit.Emails.Log.create_log(%{message_id: ""})
{:error, %Ecto.Changeset{}}

delete_log(email_log)

Deletes an email log.

Examples

iex> PhoenixKit.Emails.Log.delete_log(log)
{:ok, %PhoenixKit.Emails.Log{}}

iex> PhoenixKit.Emails.Log.delete_log(log)
{:error, %Ecto.Changeset{}}

find_by_aws_message_id(aws_message_id)

Finds an email log by AWS message ID.

This function looks for logs where the AWS SES message ID might be stored in the message_id field after sending.

Examples

iex> PhoenixKit.Emails.Log.find_by_aws_message_id("abc123-aws")
{:ok, %PhoenixKit.Emails.Log{}}

iex> PhoenixKit.Emails.Log.find_by_aws_message_id("nonexistent")
{:error, :not_found}

get_campaign_stats(campaign_id)

Gets statistics for a specific campaign.

Examples

iex> PhoenixKit.Emails.Log.get_campaign_stats("newsletter_2024")
%{total_sent: 500, delivery_rate: 96.0, open_rate: 25.0, click_rate: 5.0}

get_daily_delivery_trends(period \\ :last_7_days)

Gets daily delivery trend data for charts.

Returns daily statistics optimized for chart visualization including delivery trends and bounce patterns over the specified period.

Examples

iex> PhoenixKit.Emails.Log.get_daily_delivery_trends(:last_7_days)
%{
  labels: ["2024-09-01", "2024-09-02", ...],
  delivered: [120, 190, 300, ...],
  bounced: [5, 10, 15, ...]
}

get_engagement_metrics(period \\ :last_30_days)

Gets engagement metrics for analysis.

Examples

iex> PhoenixKit.Emails.Log.get_engagement_metrics(:last_30_days)
%{avg_open_rate: 24.5, avg_click_rate: 4.2, engagement_trend: :increasing}

get_log!(id)

Gets a single email log by ID.

Raises Ecto.NoResultsError if the log does not exist.

Examples

iex> PhoenixKit.Emails.Log.get_log!(123)
%PhoenixKit.Emails.Log{}

iex> PhoenixKit.Emails.Log.get_log!(999)
** (Ecto.NoResultsError)

get_log_by_message_id(message_id)

Gets a single email log by message ID from the email provider.

Returns nil if not found.

Examples

iex> PhoenixKit.Emails.Log.get_log_by_message_id("msg-abc123")
%PhoenixKit.Emails.Log{}

iex> PhoenixKit.Emails.Log.get_log_by_message_id("nonexistent")
nil

get_logs_for_archival(days_old \\ 90)

Gets logs ready for archival to external storage.

Examples

iex> PhoenixKit.Emails.Log.get_logs_for_archival(90)
[%PhoenixKit.Emails.Log{}, ...]

get_provider_performance(period \\ :last_7_days)

Gets provider-specific performance metrics.

Examples

iex> PhoenixKit.Emails.Log.get_provider_performance(:last_7_days)
%{"aws_ses" => %{delivered: 98.5, bounced: 1.5}, "smtp" => %{delivered: 95.0, bounced: 5.0}}

get_stats_for_period(start_date, end_date)

Gets statistics for a specific time period.

Examples

iex> PhoenixKit.Emails.Log.get_stats_for_period(~U[2024-01-01 00:00:00Z], ~U[2024-01-31 23:59:59Z])
%{total_sent: 1500, delivered: 1450, bounced: 30, opened: 800, clicked: 200}

list_logs(filters \\ %{})

Returns a list of emails with optional filters.

Filters

  • :status - Filter by status (sent, delivered, bounced, etc.)
  • :campaign_id - Filter by campaign
  • :template_name - Filter by template
  • :provider - Filter by email provider
  • :from_date - Emails sent after this date
  • :to_date - Emails sent before this date
  • :recipient - Filter by recipient email (supports partial match)
  • :limit - Limit number of results (default: 50)
  • :offset - Offset for pagination

Examples

iex> PhoenixKit.Emails.Log.list_logs(%{status: "bounced", limit: 10})
[%PhoenixKit.Emails.Log{}, ...]

mark_as_bounced(email_log, bounce_type, reason \\ nil)

Marks an email as bounced with type and reason.

Examples

iex> PhoenixKit.Emails.Log.mark_as_bounced(log, "hard", "No such user")
{:ok, %PhoenixKit.Emails.Log{}}

mark_as_clicked(email_log, link_url, clicked_at \\ nil)

Marks an email as clicked with link URL and timestamp.

Examples

iex> PhoenixKit.Emails.Log.mark_as_clicked(log, "https://example.com", DateTime.utc_now())
{:ok, %PhoenixKit.Emails.Log{}}

mark_as_delayed(email_log, delay_info \\ nil, delayed_at \\ nil)

Marks an email as delayed with delay information.

Examples

iex> PhoenixKit.Emails.Log.mark_as_delayed(log, "Temporary mailbox unavailable")
{:ok, %PhoenixKit.Emails.Log{}}

mark_as_delivered(email_log, delivered_at \\ nil)

Marks an email as delivered with timestamp.

Examples

iex> PhoenixKit.Emails.Log.mark_as_delivered(log, DateTime.utc_now())
{:ok, %PhoenixKit.Emails.Log{}}

mark_as_failed(email_log, reason, failed_at \\ nil)

Marks an email as failed with error reason.

Examples

iex> PhoenixKit.Emails.Log.mark_as_failed(log, "Connection timeout")
{:ok, %PhoenixKit.Emails.Log{}}

mark_as_opened(email_log, opened_at \\ nil)

Marks an email as opened with timestamp.

Examples

iex> PhoenixKit.Emails.Log.mark_as_opened(log, DateTime.utc_now())
{:ok, %PhoenixKit.Emails.Log{}}

mark_as_queued(email_log, queued_at \\ nil)

Marks an email as queued with timestamp.

Examples

iex> PhoenixKit.Emails.Log.mark_as_queued(log)
{:ok, %PhoenixKit.Emails.Log{}}

mark_as_rejected(email_log, reason, rejected_at \\ nil)

Marks an email as rejected by provider with reason.

Examples

iex> PhoenixKit.Emails.Log.mark_as_rejected(log, "Invalid recipient")
{:ok, %PhoenixKit.Emails.Log{}}

mark_as_sent(email_log, sent_at \\ nil)

Marks an email as sent with timestamp.

Examples

iex> PhoenixKit.Emails.Log.mark_as_sent(log)
{:ok, %PhoenixKit.Emails.Log{}}

update_log(email_log, attrs)

Updates an email log.

Examples

iex> PhoenixKit.Emails.Log.update_log(log, %{status: "delivered"})
{:ok, %PhoenixKit.Emails.Log{}}

iex> PhoenixKit.Emails.Log.update_log(log, %{to: ""})
{:error, %Ecto.Changeset{}}

update_status(email_log, status)

Updates the status of an email log.

Examples

iex> PhoenixKit.Emails.Log.update_status(log, "delivered")
{:ok, %PhoenixKit.Emails.Log{}}

validate_email_format(changeset, field)

Validates email format using basic regex pattern.