Search documentation of phoenix_kit

# PhoenixKit.Emails.Log (phoenix_kit v1.6.19)

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

- `list_logs/1` - Get emails with optional filters
- `get_log!/1` - Get an email log by ID (raises if not found)
- `get_log_by_message_id/1` - Get log by message ID from provider
- `create_log/1` - Create a new email log
- `update_log/2` - Update an existing email log
- `update_status/2` - Update log status with timestamp
- `delete_log/1` - Delete an email log

### Status Management

- `mark_as_queued/1` - Mark email as queued with timestamp
- `mark_as_sent/1` - Mark email as sent with timestamp
- `mark_as_delivered/2` - Mark email as delivered with timestamp
- `mark_as_bounced/3` - Mark as bounced with bounce type and reason
- `mark_as_rejected/2` - Mark as rejected by provider
- `mark_as_failed/2` - Mark as failed with error reason
- `mark_as_delayed/2` - Mark as delayed with delay information
- `mark_as_opened/2` - Mark as opened with timestamp
- `mark_as_clicked/3` - Mark as clicked with link and timestamp

### Analytics Functions

- `get_stats_for_period/2` - Get statistics for date range
- `get_campaign_stats/1` - Get statistics for specific campaign
- `get_engagement_metrics/1` - Calculate open/click rates
- `get_provider_performance/1` - Provider-specific metrics
- `get_bounce_analysis/1` - Detailed bounce analysis

### System Functions

- `cleanup_old_logs/1` - Remove logs older than specified days
- `compress_old_bodies/1` - Compress body_full for old emails
- `get_logs_for_archival/1` - Get logs ready for archival

## 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

change_log(email_log, attrs \\ %{})

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

changeset(email_log, attrs)

Creates a changeset for email log creation and updates.

cleanup_old_logs(days_old \\ 90)

Removes emails older than specified number of days.

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.

count_logs(filters \\ %{})

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

create_from_swoosh_email(email, opts \\ [])

Creates an email log from a Swoosh.Email struct.

create_log(attrs \\ %{})

Creates an email log.

delete_log(email_log)

Deletes an email log.

find_by_aws_message_id(aws_message_id)

Finds an email log by AWS message ID.

get_campaign_stats(campaign_id)

Gets statistics for a specific campaign.

get_daily_delivery_trends(period \\ :last_7_days)

Gets daily delivery trend data for charts.

get_engagement_metrics(period \\ :last_30_days)

Gets engagement metrics for analysis.

get_log!(id)

Gets a single email log by ID.

get_log_by_message_id(message_id)

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

get_logs_for_archival(days_old \\ 90)

Gets logs ready for archival to external storage.

get_provider_performance(period \\ :last_7_days)

Gets provider-specific performance metrics.

get_stats_for_period(start_date, end_date)

Gets statistics for a specific time period.

list_logs(filters \\ %{})

Returns a list of emails with optional filters.

mark_as_bounced(email_log, bounce_type, reason \\ nil)

Marks an email as bounced with type and reason.

mark_as_clicked(email_log, link_url, clicked_at \\ nil)

Marks an email as clicked with link URL and timestamp.

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

Marks an email as delayed with delay information.

mark_as_delivered(email_log, delivered_at \\ nil)

Marks an email as delivered with timestamp.

mark_as_failed(email_log, reason, failed_at \\ nil)

Marks an email as failed with error reason.

mark_as_opened(email_log, opened_at \\ nil)

Marks an email as opened with timestamp.

mark_as_queued(email_log, queued_at \\ nil)

Marks an email as queued with timestamp.

mark_as_rejected(email_log, reason, rejected_at \\ nil)

Marks an email as rejected by provider with reason.

mark_as_sent(email_log, sent_at \\ nil)

Marks an email as sent with timestamp.

update_log(email_log, attrs)

Updates an email log.

update_status(email_log, status)

Updates the status of an email log.

validate_email_format(changeset, field)

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.