Packages
ash_typescript
0.6.1
0.17.3
0.17.2
0.17.1
0.17.0
0.16.0
0.15.3
0.15.2
retired
0.15.1
retired
0.15.0
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
retired
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.6
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.2
0.10.1
0.10.0
0.9.1
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.1
0.7.0
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.4.0
0.3.3
0.3.2
0.3.1
0.2.0
0.1.2
0.1.0
Generate type-safe TypeScript clients directly from your Ash resources and actions, ensuring end-to-end type safety between your backend and frontend.
Current section
Files
Jump to
Current section
Files
documentation/topics/lifecycle-hooks.md
<!--
SPDX-FileCopyrightText: 2025 Torkild G. Kjevik
SPDX-FileCopyrightText: 2025 ash_typescript contributors <https://github.com/ash-project/ash_typescript/graphs.contributors>
SPDX-License-Identifier: MIT
-->
# Lifecycle Hooks
AshTypescript provides comprehensive lifecycle hooks for both HTTP and Phoenix Channel-based RPC actions. These hooks enable cross-cutting concerns like logging, telemetry, performance tracking, and error monitoring. HTTP hooks additionally support authentication header injection.
## Table of Contents
- [HTTP Lifecycle Hooks](#http-lifecycle-hooks)
- [Why Use HTTP Lifecycle Hooks?](#why-use-http-lifecycle-hooks)
- [HTTP Configuration](#http-configuration)
- [Hook Types: Actions vs Validations](#hook-types-actions-vs-validations)
- [Hook Function Signatures](#hook-function-signatures)
- [beforeRequest Hook](#beforerequest-hook)
- [afterRequest Hook](#afterrequest-hook)
- [Config Precedence Rules](#config-precedence-rules)
- [Exception Handling](#exception-handling)
- [Complete Working Example](#complete-working-example)
- [Channel Lifecycle Hooks](#channel-lifecycle-hooks)
- [Why Use Channel Lifecycle Hooks?](#why-use-channel-lifecycle-hooks)
- [Key Differences from HTTP Hooks](#key-differences-from-http-hooks)
- [Channel Configuration](#channel-configuration)
- [Channel Hook Function Signatures](#channel-hook-function-signatures)
- [beforeChannelPush Hook](#beforechannelpush-hook)
- [afterChannelResponse Hook](#afterchannelresponse-hook)
- [Channel Config Precedence Rules](#channel-config-precedence-rules)
- [Complete Channel Working Example](#complete-channel-working-example)
- [Troubleshooting](#troubleshooting)
## HTTP Lifecycle Hooks
AshTypescript provides lifecycle hooks that let you inject custom logic before and after HTTP requests. These hooks enable cross-cutting concerns like authentication, logging, telemetry, performance tracking, and error monitoring.
### Why Use HTTP Lifecycle Hooks?
Lifecycle hooks provide a centralized way to:
- **Add authentication tokens** - Automatically inject auth headers from localStorage
- **Log requests and responses** - Track API calls for debugging
- **Measure performance** - Time API calls and track latency
- **Send telemetry** - Report metrics to monitoring services
- **Handle errors globally** - Track errors in Sentry, Datadog, etc.
- **Add correlation IDs** - Track requests across distributed systems
- **Add default headers** - Set client version, request IDs, etc.
- **Transform requests** - Modify config before sending
### HTTP Configuration
Configure lifecycle hooks in your application config:
```elixir
# config/config.exs
config :ash_typescript,
# Hook functions for RPC actions
rpc_action_before_request_hook: "RpcHooks.beforeRequest",
rpc_action_after_request_hook: "RpcHooks.afterRequest",
# Hook functions for validation actions
rpc_validation_before_request_hook: "RpcHooks.beforeValidationRequest",
rpc_validation_after_request_hook: "RpcHooks.afterValidationRequest",
# TypeScript types for hook context (optional)
rpc_action_hook_context_type: "RpcHooks.ActionHookContext",
rpc_validation_hook_context_type: "RpcHooks.ValidationHookContext",
# Import the module containing your hook functions
import_into_generated: [
%{
import_name: "RpcHooks",
file: "./rpcHooks"
}
]
```
**Configuration Options:**
| Config | Purpose | Default |
|--------|---------|---------|
| `rpc_action_before_request_hook` | Function called before RPC action requests | `nil` (disabled) |
| `rpc_action_after_request_hook` | Function called after RPC action requests | `nil` (disabled) |
| `rpc_validation_before_request_hook` | Function called before validation requests | `nil` (disabled) |
| `rpc_validation_after_request_hook` | Function called after validation requests | `nil` (disabled) |
| `rpc_action_hook_context_type` | TypeScript type for action hook context | `"Record<string, any>"` |
| `rpc_validation_hook_context_type` | TypeScript type for validation hook context | `"Record<string, any>"` |
### Hook Types: Actions vs Validations
AshTypescript provides **separate hooks for actions and validations** because they serve different purposes:
- **Action Hooks** - Execute when calling RPC actions (create, read, update, delete, custom actions)
- **Validation Hooks** - Execute when calling validation functions (client-side form validation)
This separation allows you to:
- Use different logging levels (validations are typically more frequent)
- Track different metrics (validation performance vs action performance)
**Action hooks are for actual API calls, validation hooks are for form validation.**
### Hook Function Signatures
Both `beforeRequest` and `afterRequest` hooks receive the full config object and can access the optional `hookCtx` from it:
```typescript
// Config interface showing all available fields
interface ActionConfig {
// Request data
fields?: any; // Field selection
input?: Record<string, any>; // Input data (for mutations)
primaryKey?: any; // Primary key (for get/update/destroy)
filter?: Record<string, any>; // Filter options (for reads)
sort?: string; // Sort options
page?: { // Pagination options
limit?: number;
offset?: number;
count?: boolean;
};
// Metadata
metadataFields?: string[]; // Metadata field selection
// HTTP customization
headers?: Record<string, string>; // Custom headers
fetchOptions?: RequestInit; // Fetch options (signal, cache, etc.)
customFetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
// Multitenancy
tenant?: string; // Tenant parameter
// Hook context
hookCtx?: any; // Custom hook context
// Internal fields (available but typically not modified)
action?: string; // Action name
domain?: string; // Domain name
}
// Validation config (for validation hooks)
interface ValidationConfig {
// Request data
input?: Record<string, any>; // Input data
// HTTP customization
headers?: Record<string, string>; // Custom headers
fetchOptions?: RequestInit; // Fetch options (signal, cache, etc.)
customFetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
// Hook context
hookCtx?: any; // Custom hook context (typically ValidationHookContext)
// Internal fields
action?: string; // Action name
domain?: string; // Domain name
}
// Before request hook signature
function beforeRequest(config: ActionConfig): ActionConfig {
// Access optional hook context
const ctx = config.hookCtx;
// Return modified config (or original config)
return { ...config, /* modifications */ };
}
// After request hook signature
function afterRequest(
response: Response,
result: any | null, // null when response.ok is false
config: ActionConfig
): void {
// Access optional hook context
const ctx = config.hookCtx;
// Perform side effects (logging, telemetry, etc.)
// No return value
}
```
**Key Points:**
- Hooks receive the entire `config` object as a parameter
- Hook context is accessed via `config.hookCtx` (optional)
- `beforeRequest` returns a modified config object
- `afterRequest` returns nothing (void) - it's for side effects only
- Hooks run unconditionally when configured (not gated by `hookCtx` presence)
### beforeRequest Hook
The `beforeRequest` hook runs **before the HTTP request** and can modify the request configuration. Common use cases:
#### Adding Authentication Tokens
```typescript
// rpcHooks.ts
export function beforeRequest<T extends ActionConfig>(config: T): T {
// Fetch auth token from localStorage (if it exists)
const authToken = localStorage.getItem('authToken');
// Add authentication header if token is present
if (authToken) {
return {
...config,
headers: {
...config.headers,
'Authorization': `Bearer ${authToken}`
}
} as T;
}
return config;
}
```
This pattern automatically adds authentication to all RPC requests without needing to pass tokens through every call. The hook centralizes auth header logic in one place.
```typescript
// Usage: Auth headers are added automatically
const todos = await listTodos({
fields: ["id", "title"]
// No need to pass auth tokens - hook handles it!
});
```
#### Adding Correlation IDs for Request Tracking
```typescript
// rpcHooks.ts
export interface ActionHookContext {
correlationId?: string;
}
export function beforeRequest<T extends ActionConfig>(config: T): T {
const ctx = config.hookCtx;
// Use provided correlation ID or generate one
const correlationId = ctx?.correlationId || generateRequestId();
return {
...config,
headers: {
'X-Client-Version': '1.0.0',
'X-Correlation-ID': correlationId,
'X-Request-ID': correlationId,
...config.headers // Original headers take precedence
}
} as T;
}
function generateRequestId(): string {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
```
```typescript
// Usage: Pass correlation ID for distributed tracing
const todos = await listTodos({
fields: ["id", "title"],
hookCtx: {
correlationId: 'user-dashboard-load-456'
}
});
```
#### Request Timing Setup
```typescript
export interface ActionHookContext {
startTime?: number;
}
export function beforeRequest<T extends ActionConfig>(config: T): T {
const ctx = config.hookCtx;
// Store request start time in context for afterRequest hook
if (ctx) {
ctx.startTime = Date.now();
}
return config;
}
```
#### Logging Outgoing Requests
```typescript
export function beforeRequest<T extends ActionConfig>(config: T): T {
const ctx = config.hookCtx;
console.log('Outgoing RPC request:', {
action: config.action,
domain: config.domain,
hasInput: !!config.input,
timestamp: new Date().toISOString(),
correlationId: ctx?.correlationId
});
return config;
}
```
### afterRequest Hook
The `afterRequest` hook runs **after the HTTP request completes** (both success and error) and is used for side effects. It receives three parameters:
1. `response: Response` - The raw HTTP response object
2. `result: any | null` - Parsed JSON result (null when `response.ok` is false)
3. `config: ActionConfig` - The config used for the request
#### Important: Null Result Handling
The `afterRequest` hook receives `null` as the result parameter when the response is not OK:
```typescript
export function afterRequest<T extends ActionConfig>(
response: Response,
result: any | null,
config: T
): void {
if (result === null) {
// Response failed (response.ok === false)
console.error('Request failed:', {
status: response.status,
statusText: response.statusText,
url: response.url
});
} else {
// Response succeeded (response.ok === true)
console.log('Request succeeded:', {
hasData: !!result.data,
success: result.success
});
}
}
```
#### Logging All Responses
```typescript
export function afterRequest<T extends ActionConfig>(
response: Response,
result: any | null,
config: T
): void {
const ctx = config.hookCtx;
console.log('RPC response received:', {
action: config.action,
domain: config.domain,
status: response.status,
ok: response.ok,
hasResult: result !== null,
correlationId: ctx?.correlationId,
timestamp: new Date().toISOString()
});
}
```
#### Performance Timing
```typescript
export interface ActionHookContext {
startTime?: number;
trackPerformance?: boolean;
}
export function afterRequest<T extends ActionConfig>(
response: Response,
result: any | null,
config: T
): void {
const ctx = config.hookCtx;
if (ctx?.trackPerformance && ctx.startTime) {
const duration = Date.now() - ctx.startTime;
console.log('Performance metrics:', {
action: config.action,
duration: `${duration}ms`,
status: response.status,
success: result !== null && result.success
});
// Send to analytics service
trackMetric('rpc.duration', duration, {
action: config.action,
status: response.status
});
}
}
```
#### Telemetry Tracking
```typescript
export function afterRequest<T extends ActionConfig>(
response: Response,
result: any | null,
config: T
): void {
// Send telemetry to monitoring service
sendTelemetry({
event: 'rpc.request.completed',
action: config.action,
domain: config.domain,
status: response.status,
success: response.ok && result?.success,
timestamp: Date.now()
});
}
```
#### Error Monitoring
```typescript
export function afterRequest<T extends ActionConfig>(
response: Response,
result: any | null,
config: T
): void {
// Track errors in error monitoring service
if (result === null || !result.success) {
Sentry.captureMessage('RPC request failed', {
level: 'error',
extra: {
action: config.action,
status: response.status,
errors: result?.errors,
url: response.url
}
});
}
}
```
### Config Precedence Rules
When using `beforeRequest` hooks, the **original config always takes precedence** over the modified config:
```typescript
export function beforeRequest<T extends ActionConfig>(config: T): T {
return {
...config,
headers: {
'X-Default-Header': 'value',
...config.headers // ← Original headers override defaults
},
customFetch: config.customFetch || myDefaultFetch // ← Original takes precedence
} as T;
}
```
**Precedence order:**
1. Original `config` values (highest priority)
2. Modified config from `beforeRequest` hook
3. Default fetch implementation (lowest priority)
This ensures that per-request customizations always override hook defaults.
### Exception Handling
Hooks **do not catch exceptions** - any errors thrown by hooks will propagate to the caller:
```typescript
export function beforeRequest<T extends ActionConfig>(config: T): T {
if (!isValidConfig(config)) {
// This exception propagates to the caller
throw new Error('Invalid RPC configuration');
}
return config;
}
```
**Use Cases for Exception Propagation:**
1. **Error Boundaries** - Let framework error boundaries catch and display errors
2. **Global Error Handlers** - Centralized error handling in your app
3. **Fail-Fast Validation** - Stop execution on critical errors
```typescript
// React component with error boundary
function MyComponent() {
const handleSubmit = async () => {
try {
const result = await createTodo({
fields: ["id", "title"],
input: {
title: "New Todo",
userId: "123e4567-e89b-12d3-a456-426614174000"
},
hookCtx: {
correlationId: 'user-submit-action',
trackPerformance: true
}
});
// Handle success
} catch (error) {
// Hook threw an exception
console.error('RPC call failed:', error);
}
};
}
```
### Complete Working Example
Here's a complete example showing all hook features:
```typescript
// rpcHooks.ts
export interface ActionHookContext {
trackPerformance?: boolean;
startTime?: number;
correlationId?: string;
}
export interface ValidationHookContext {
formId?: string;
}
// Action hooks
export function beforeRequest<T extends ActionConfig>(config: T): T {
const ctx = config.hookCtx;
// Add correlation ID and client version headers
const headers: Record<string, string> = {
'X-Client-Version': '1.0.0',
'X-Correlation-ID': ctx?.correlationId || generateRequestId(),
...config.headers
};
// Setup timing for performance tracking
if (ctx?.trackPerformance && ctx) {
ctx.startTime = Date.now();
}
console.log(`[RPC] ${config.action} started`, {
correlationId: ctx?.correlationId
});
return { ...config, headers } as T;
}
function generateRequestId(): string {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
export function afterRequest<T extends ActionConfig>(
response: Response,
result: any | null,
config: T
): void {
const ctx = config.hookCtx;
// Track timing
const duration = ctx?.startTime ? Date.now() - ctx.startTime : 0;
// Log result
if (result === null) {
console.error(`[RPC] ${config.action} failed:`, {
status: response.status,
duration: `${duration}ms`
});
} else {
console.log(`[RPC] ${config.action} completed:`, {
success: result.success,
duration: `${duration}ms`
});
}
}
// Validation hooks
export function beforeValidationRequest<T extends ValidationConfig>(config: T): T {
const ctx = config.hookCtx;
console.log(`[Validation] ${config.action} started`, { formId: ctx?.formId });
return config;
}
export function afterValidationRequest<T extends ValidationConfig>(
response: Response,
result: any | null,
config: T
): void {
const ctx = config.hookCtx;
console.log(`[Validation] ${config.action} completed`, {
formId: ctx?.formId,
hasErrors: result && !result.success
});
}
```
```typescript
// Usage in your application
import { createTodo, validateCreateTodo } from './ash_rpc';
// Action with hooks
const result = await createTodo({
fields: ["id", "title", "createdAt"],
input: {
title: "Learn AshTypescript Hooks",
userId: getCurrentUserId()
},
hookCtx: {
trackPerformance: true,
correlationId: 'user-create-todo-123'
}
});
// Validation with hooks
const validationResult = await validateCreateTodo({
input: {
title: "Test Todo",
userId: "123e4567-e89b-12d3-a456-426614174000"
},
hookCtx: {
formId: 'create-todo-form'
}
});
```
## Channel Lifecycle Hooks
AshTypescript provides lifecycle hooks for Phoenix Channel-based RPC actions, mirroring the HTTP hooks functionality but adapted for real-time channel communication. These hooks enable the same cross-cutting concerns (logging, telemetry, performance tracking, error monitoring) but for WebSocket-based communication instead of HTTP requests.
### Why Use Channel Lifecycle Hooks?
Channel lifecycle hooks provide a centralized way to:
- **Log channel messages** - Track channel communication for debugging
- **Measure performance** - Time channel operations and track latency
- **Send telemetry** - Report metrics to monitoring services
- **Handle errors globally** - Track channel errors in Sentry, Datadog, etc.
- **Add default configuration** - Set default timeouts or other options
- **Transform messages** - Modify config before pushing to channel
### Key Differences from HTTP Hooks
Channel hooks differ from HTTP hooks because they work with Phoenix Channel's message-based communication:
| Aspect | HTTP Hooks | Channel Hooks |
|--------|-----------|---------------|
| **Communication** | Request/Response (HTTP) | Message-based (WebSocket) |
| **API Style** | Promise-based | Callback-based |
| **Response Types** | Success or Error | ok, error, or timeout |
| **Hook Names** | `beforeRequest`, `afterRequest` | `beforeChannelPush`, `afterChannelResponse` |
### Channel Configuration
Configure channel lifecycle hooks in your application config:
```elixir
# config/config.exs
config :ash_typescript,
# Channel-based hooks for RPC actions
rpc_action_before_channel_push_hook: "ChannelHooks.beforeChannelPush",
rpc_action_after_channel_response_hook: "ChannelHooks.afterChannelResponse",
# Channel-based hooks for validation actions
rpc_validation_before_channel_push_hook: "ChannelHooks.beforeValidationChannelPush",
rpc_validation_after_channel_response_hook: "ChannelHooks.afterValidationChannelResponse",
# TypeScript types for channel hook context (optional)
rpc_action_channel_hook_context_type: "ChannelHooks.ActionChannelHookContext",
rpc_validation_channel_hook_context_type: "ChannelHooks.ValidationChannelHookContext",
# Import the module containing your channel hook functions
import_into_generated: [
%{
import_name: "ChannelHooks",
file: "./channelHooks"
}
]
```
**Configuration Options:**
| Config | Purpose | Default |
|--------|---------|---------|
| `rpc_action_before_channel_push_hook` | Function called before channel push for RPC actions | `nil` (disabled) |
| `rpc_action_after_channel_response_hook` | Function called after channel response for RPC actions | `nil` (disabled) |
| `rpc_validation_before_channel_push_hook` | Function called before channel push for validations | `nil` (disabled) |
| `rpc_validation_after_channel_response_hook` | Function called after channel response for validations | `nil` (disabled) |
| `rpc_action_channel_hook_context_type` | TypeScript type for action channel hook context | `"Record<string, any>"` |
| `rpc_validation_channel_hook_context_type` | TypeScript type for validation channel hook context | `"Record<string, any>"` |
### Channel Hook Function Signatures
Channel hooks receive the full config object and can access the optional `hookCtx` from it:
```typescript
// Channel config interface showing all available fields
interface ChannelActionConfig {
// Channel connection
channel: Channel; // Phoenix channel instance
// Request data
fields?: any; // Field selection
input?: Record<string, any>; // Input data (for mutations)
primaryKey?: any; // Primary key (for get/update/destroy)
filter?: Record<string, any>; // Filter options (for reads)
sort?: string; // Sort options
page?: { // Pagination options
limit?: number;
offset?: number;
count?: boolean;
};
// Metadata
metadataFields?: string[]; // Metadata field selection
// Channel options
timeout?: number; // Message timeout (milliseconds)
// Handlers
resultHandler: (result: any) => void; // Called on "ok" response
errorHandler?: (error: any) => void; // Called on "error" response
timeoutHandler?: () => void; // Called on "timeout" response
// Multitenancy
tenant?: string; // Tenant parameter
// Hook context
hookCtx?: any; // Custom hook context
// Internal fields (available but typically not modified)
action?: string; // Action name
domain?: string; // Domain name
}
// Before channel push hook signature
function beforeChannelPush(
actionName: string,
config: ChannelActionConfig
): ChannelActionConfig | Promise<ChannelActionConfig> {
// Access optional hook context
const ctx = config.hookCtx;
// Return modified config (or original config)
return { ...config, /* modifications */ };
}
// After channel response hook signature
function afterChannelResponse(
actionName: string,
responseType: "ok" | "error" | "timeout",
data: any, // result (for ok), error (for error), or null (for timeout)
config: ChannelActionConfig
): void | Promise<void> {
// Access optional hook context
const ctx = config.hookCtx;
// Perform side effects (logging, telemetry, etc.)
// No return value
}
```
**Key Points:**
- Channel hooks support async operations (Promise-based)
- `beforeChannelPush` receives action name and config, returns modified config
- `afterChannelResponse` receives action name, response type, data, and config
- Response type distinguishes between three channel outcomes: "ok", "error", "timeout"
- Original config takes precedence over modified config
### beforeChannelPush Hook
The `beforeChannelPush` hook runs **before the channel.push()** call and can modify the channel message configuration. Common use cases:
#### Setting Default Timeout
```typescript
// channelHooks.ts
export interface ActionChannelHookContext {
useDefaultTimeout?: boolean;
customTimeout?: number;
}
export async function beforeChannelPush<T extends ChannelActionConfig>(
actionName: string,
config: T
): Promise<T> {
const ctx = config.hookCtx;
// Set default timeout if not specified
if (ctx?.useDefaultTimeout && !config.timeout) {
return {
...config,
timeout: ctx.customTimeout || 10000 // 10 second default
} as T;
}
return config;
}
```
```typescript
// Usage: Pass timeout preferences via hook context
listTodosChannel({
channel: myChannel,
fields: ["id", "title"],
hookCtx: {
useDefaultTimeout: true,
customTimeout: 15000
},
resultHandler: (result) => console.log(result)
});
```
#### Logging Channel Messages
```typescript
export interface ActionChannelHookContext {
correlationId?: string;
trackPerformance?: boolean;
startTime?: number;
}
export async function beforeChannelPush<T extends ChannelActionConfig>(
actionName: string,
config: T
): Promise<T> {
const ctx = config.hookCtx;
// Setup timing
if (ctx?.trackPerformance && ctx) {
ctx.startTime = Date.now();
}
console.log(`[Channel] Pushing to channel:`, {
action: actionName,
correlationId: ctx?.correlationId,
timestamp: new Date().toISOString()
});
return config;
}
```
### afterChannelResponse Hook
The `afterChannelResponse` hook runs **after the channel response is received** (ok, error, or timeout) and is used for side effects. It receives four parameters:
1. `actionName: string` - The name of the action being executed
2. `responseType: "ok" | "error" | "timeout"` - The type of channel response
3. `data: any` - Response data (result for "ok", error for "error", null for "timeout")
4. `config: ChannelActionConfig` - The config used for the request
#### Logging All Channel Responses
```typescript
export async function afterChannelResponse<T extends ChannelActionConfig>(
actionName: string,
responseType: "ok" | "error" | "timeout",
data: any,
config: T
): Promise<void> {
const ctx = config.hookCtx;
console.log(`[Channel] Response received:`, {
action: actionName,
responseType,
hasData: data !== null,
correlationId: ctx?.correlationId,
timestamp: new Date().toISOString()
});
// Log specific details based on response type
if (responseType === "error") {
console.error(`[Channel] Error in ${actionName}:`, data);
} else if (responseType === "timeout") {
console.warn(`[Channel] Timeout in ${actionName}`);
}
}
```
#### Performance Timing
```typescript
export interface ActionChannelHookContext {
startTime?: number;
trackPerformance?: boolean;
correlationId?: string;
}
export async function afterChannelResponse<T extends ChannelActionConfig>(
actionName: string,
responseType: "ok" | "error" | "timeout",
data: any,
config: T
): Promise<void> {
const ctx = config.hookCtx;
if (ctx?.trackPerformance && ctx.startTime) {
const duration = Date.now() - ctx.startTime;
console.log(`[Channel] Performance metrics:`, {
action: actionName,
duration: `${duration}ms`,
responseType,
success: responseType === "ok" && data?.success,
correlationId: ctx?.correlationId
});
// Send to analytics service
trackMetric('channel.rpc.duration', duration, {
action: actionName,
responseType,
success: responseType === "ok"
});
}
}
```
#### Telemetry Tracking
```typescript
export async function afterChannelResponse<T extends ChannelActionConfig>(
actionName: string,
responseType: "ok" | "error" | "timeout",
data: any,
config: T
): Promise<void> {
// Send telemetry to monitoring service
sendTelemetry({
event: 'channel.rpc.completed',
action: actionName,
domain: config.domain,
responseType,
success: responseType === "ok" && data?.success,
timestamp: Date.now()
});
// Track specific response types
if (responseType === "timeout") {
sendTelemetry({
event: 'channel.rpc.timeout',
action: actionName,
timestamp: Date.now()
});
}
}
```
#### Error Monitoring
```typescript
export async function afterChannelResponse<T extends ChannelActionConfig>(
actionName: string,
responseType: "ok" | "error" | "timeout",
data: any,
config: T
): Promise<void> {
// Track errors in error monitoring service
if (responseType === "error" || responseType === "timeout") {
Sentry.captureMessage('Channel RPC failed', {
level: 'error',
extra: {
action: actionName,
responseType,
data: responseType === "error" ? data : null,
domain: config.domain
}
});
} else if (data && !data.success) {
// Track validation errors from successful channel responses
Sentry.captureMessage('Channel RPC validation error', {
level: 'warning',
extra: {
action: actionName,
errors: data.errors
}
});
}
}
```
### Channel Config Precedence Rules
When using `beforeChannelPush` hooks, the **original config always takes precedence** over the modified config:
```typescript
export async function beforeChannelPush<T extends ChannelActionConfig>(
actionName: string,
config: T
): Promise<T> {
return {
...config,
timeout: config.timeout ?? 10000 // ← Original timeout takes precedence
} as T;
}
```
**Precedence order:**
1. Original `config` values (highest priority)
2. Modified config from `beforeChannelPush` hook
3. No default timeout (lowest priority)
This ensures that per-request customizations always override hook defaults.
### Complete Channel Working Example
Here's a complete example showing all channel hook features:
```typescript
// channelHooks.ts
export interface ActionChannelHookContext {
trackPerformance?: boolean;
startTime?: number;
correlationId?: string;
}
export interface ValidationChannelHookContext {
formId?: string;
validationLevel?: "strict" | "normal";
}
// Action hooks
export async function beforeChannelPush<T extends ChannelActionConfig>(
actionName: string,
config: T
): Promise<T> {
const ctx = config.hookCtx;
// Setup timing
if (ctx?.trackPerformance && ctx) {
ctx.startTime = Date.now();
}
console.log(`[Channel] ${actionName} starting`, {
correlationId: ctx?.correlationId
});
return config;
}
export async function afterChannelResponse<T extends ChannelActionConfig>(
actionName: string,
responseType: "ok" | "error" | "timeout",
data: any,
config: T
): Promise<void> {
const ctx = config.hookCtx;
// Track timing
const duration = ctx?.startTime ? Date.now() - ctx.startTime : 0;
// Log result
console.log(`[Channel] ${actionName} completed:`, {
responseType,
duration: `${duration}ms`,
correlationId: ctx?.correlationId
});
// Track errors
if (responseType !== "ok") {
console.error(`[Channel] ${actionName} failed:`, { responseType, data });
}
}
// Validation hooks
export async function beforeValidationChannelPush<T extends ValidationChannelConfig>(
actionName: string,
config: T
): Promise<T> {
const ctx = config.hookCtx;
console.log(`[Channel Validation] ${actionName} started`, {
formId: ctx?.formId,
validationLevel: ctx?.validationLevel
});
return config;
}
export async function afterValidationChannelResponse<T extends ValidationChannelConfig>(
actionName: string,
responseType: "ok" | "error" | "timeout",
data: any,
config: T
): Promise<void> {
const ctx = config.hookCtx;
console.log(`[Channel Validation] ${actionName} completed`, {
formId: ctx?.formId,
responseType,
hasErrors: responseType === "ok" && data && !data.success
});
}
```
```typescript
// Usage in your application
import { listTodosChannel, createTodoChannel, validateCreateTodoChannel } from './ash_rpc';
import { Channel } from "phoenix";
// Action with channel hooks
listTodosChannel({
channel: myChannel,
fields: ["id", "title", { user: ["name"] }],
hookCtx: {
trackPerformance: true,
correlationId: 'list-todos-123'
},
resultHandler: (result) => {
if (result.success) {
console.log("Todos loaded:", result.data);
}
}
});
// Validation with channel hooks
validateCreateTodoChannel({
channel: myChannel,
input: {
title: "Test Todo",
userId: "123e4567-e89b-12d3-a456-426614174000"
},
hookCtx: {
formId: 'create-todo-form',
validationLevel: 'strict'
},
resultHandler: (result) => {
if (!result.success) {
console.log("Validation errors:", result.errors);
}
}
});
```
## Troubleshooting
### HTTP Hooks
**Config precedence not working:**
```typescript
// ❌ Wrong: Original config gets overridden
return {
headers: { ...config.headers, 'X-Custom': 'value' },
...config
};
// ✅ Correct: Original config takes precedence
return {
...config,
headers: { 'X-Custom': 'value', ...config.headers }
};
```
**Performance timing not working:**
```typescript
// ❌ Wrong: Context is read-only, modifications lost
export function beforeRequest<T extends ActionConfig>(config: T): T {
const ctx = config.hookCtx;
ctx.startTime = Date.now(); // Lost!
return config;
}
// ✅ Correct: Return modified context
export function beforeRequest<T extends ActionConfig>(config: T): T {
const ctx = config.hookCtx || {};
return {
...config,
hookCtx: { ...ctx, startTime: Date.now() }
} as T;
}
```
**Hook not executing:**
- Verify hook functions are exported from the configured module
- Check that `import_into_generated` includes the hooks module
- Regenerate types with `mix ash.codegen --dev`
- Ensure hook function names match the configuration exactly
**TypeScript errors with hook context:**
```typescript
// ❌ Wrong: Type assertion on config
const ctx = config.hookCtx as ActionHookContext;
ctx.trackPerformance; // Error if hookCtx is undefined
// ✅ Correct: Optional chaining or type guard
const ctx = config.hookCtx as ActionHookContext | undefined;
if (ctx?.trackPerformance) {
// Safe to use
}
```
### Channel Hooks
**Config precedence not working:**
```typescript
// ❌ Wrong: Original config gets overridden
return {
timeout: 10000,
...config
};
// ✅ Correct: Original config takes precedence
return {
...config,
timeout: config.timeout ?? 10000
};
```
**Hook not executing:**
- Verify channel hook functions are exported from the configured module
- Check that `import_into_generated` includes the channel hooks module
- Regenerate types with `mix ash.codegen --dev`
- Ensure hook function names match the configuration exactly
- Verify that `generate_phx_channel_rpc_actions: true` is set in config
**TypeScript errors with channel hook context:**
```typescript
// ❌ Wrong: Type assertion without null check
const ctx = config.hookCtx as ActionChannelHookContext;
ctx.trackPerformance; // Error if hookCtx is undefined
// ✅ Correct: Optional chaining or type guard
const ctx = config.hookCtx as ActionChannelHookContext | undefined;
if (ctx?.trackPerformance) {
// Safe to use
}
```
**Response type not being handled:**
```typescript
// ✅ Handle all three response types
export async function afterChannelResponse(
actionName: string,
responseType: "ok" | "error" | "timeout",
data: any,
config: any
): Promise<void> {
switch (responseType) {
case "ok":
// Handle successful response
break;
case "error":
// Handle error response
break;
case "timeout":
// Handle timeout response
break;
}
}
```