Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions example/basic/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,48 @@ void main(List<String> args) {
},
);

// ==========================================================================
// Firebase Alerts trigger examples
// ==========================================================================

// Crashlytics new fatal issue alert
firebase.alerts.crashlytics.onNewFatalIssuePublished(
(event) async {
final issue = event.data?.payload.issue;
print('New fatal issue in Crashlytics:');
print(' Issue ID: ${issue?.id}');
print(' Title: ${issue?.title}');
print(' App Version: ${issue?.appVersion}');
print(' App ID: ${event.appId}');
},
);

// Billing plan update alert
firebase.alerts.billing.onPlanUpdatePublished(
(event) async {
final payload = event.data?.payload;
print('Billing plan updated:');
print(' New Plan: ${payload?.billingPlan}');
print(' Updated By: ${payload?.principalEmail}');
print(' Type: ${payload?.notificationType}');
},
);

// Performance threshold alert with app ID filter
firebase.alerts.performance.onThresholdAlertPublished(
options: const AlertOptions(appId: '1:123456789:ios:abcdef'),
(event) async {
final payload = event.data?.payload;
print('Performance threshold exceeded:');
print(' Event: ${payload?.eventName}');
print(' Metric: ${payload?.metricType}');
print(
' Threshold: ${payload?.thresholdValue} ${payload?.thresholdUnit}',
);
print(' Actual: ${payload?.violationValue} ${payload?.violationUnit}');
},
);

print('Functions registered successfully!');
});
}
234 changes: 233 additions & 1 deletion lib/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ class _TypeCheckers {
TypeChecker.fromRuntime(ff.FirestoreNamespace);
static final databaseNamespace =
TypeChecker.fromRuntime(ff.DatabaseNamespace);
static final alertsNamespace = TypeChecker.fromRuntime(ff.AlertsNamespace);
static final crashlyticsNamespace =
TypeChecker.fromRuntime(ff.CrashlyticsNamespace);
static final billingNamespace = TypeChecker.fromRuntime(ff.BillingNamespace);
static final appDistributionNamespace =
TypeChecker.fromRuntime(ff.AppDistributionNamespace);
static final performanceNamespace =
TypeChecker.fromRuntime(ff.PerformanceNamespace);
}

/// The main builder that generates functions.yaml.
Expand Down Expand Up @@ -144,6 +152,33 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor<void> {
}
}

// Check for Alerts function declarations (main namespace)
if (target != null && _isAlertsNamespace(target)) {
if (methodName == 'onAlertPublished') {
_extractGenericAlertFunction(node);
}
}

// Check for Crashlytics alert declarations
if (target != null && _isCrashlyticsNamespace(target)) {
_extractCrashlyticsAlertFunction(node, methodName);
}

// Check for Billing alert declarations
if (target != null && _isBillingNamespace(target)) {
_extractBillingAlertFunction(node, methodName);
}

// Check for App Distribution alert declarations
if (target != null && _isAppDistributionNamespace(target)) {
_extractAppDistributionAlertFunction(node, methodName);
}

// Check for Performance alert declarations
if (target != null && _isPerformanceNamespace(target)) {
_extractPerformanceAlertFunction(node, methodName);
}

// Check for parameter definitions (top-level function calls with no target)
if (target == null && _isParamDefinition(methodName)) {
_extractParameterFromMethod(node, methodName);
Expand Down Expand Up @@ -255,6 +290,41 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor<void> {
return _TypeCheckers.databaseNamespace.isExactlyType(staticType);
}

/// Checks if the target is firebase.alerts.
bool _isAlertsNamespace(Expression target) {
final staticType = target.staticType;
if (staticType == null) return false;
return _TypeCheckers.alertsNamespace.isExactlyType(staticType);
}

/// Checks if the target is firebase.alerts.crashlytics.
bool _isCrashlyticsNamespace(Expression target) {
final staticType = target.staticType;
if (staticType == null) return false;
return _TypeCheckers.crashlyticsNamespace.isExactlyType(staticType);
}

/// Checks if the target is firebase.alerts.billing.
bool _isBillingNamespace(Expression target) {
final staticType = target.staticType;
if (staticType == null) return false;
return _TypeCheckers.billingNamespace.isExactlyType(staticType);
}

/// Checks if the target is firebase.alerts.appDistribution.
bool _isAppDistributionNamespace(Expression target) {
final staticType = target.staticType;
if (staticType == null) return false;
return _TypeCheckers.appDistributionNamespace.isExactlyType(staticType);
}

/// Checks if the target is firebase.alerts.performance.
bool _isPerformanceNamespace(Expression target) {
final staticType = target.staticType;
if (staticType == null) return false;
return _TypeCheckers.performanceNamespace.isExactlyType(staticType);
}

/// Checks if this is a parameter definition function.
bool _isParamDefinition(String name) =>
name == 'defineString' ||
Expand Down Expand Up @@ -393,6 +463,152 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor<void> {
);
}

/// Extracts a generic alert function declaration (onAlertPublished).
void _extractGenericAlertFunction(MethodInvocation node) {
// Extract alertType from named argument
final alertTypeArg = _findNamedArg(node, 'alertType');
if (alertTypeArg == null) return;

final alertTypeValue = _extractAlertTypeValue(alertTypeArg);
if (alertTypeValue == null) return;

// Extract appId from options if present
final optionsArg = _findNamedArg(node, 'options');
String? appId;

if (optionsArg is InstanceCreationExpression) {
appId = _extractStringField(optionsArg, 'appId');
}

// Generate function name
final sanitizedAlertType =
alertTypeValue.replaceAll('.', '_').replaceAll('-', '');
final functionName = 'onAlertPublished_$sanitizedAlertType';

endpoints[functionName] = _EndpointSpec(
name: functionName,
type: 'alert',
alertType: alertTypeValue,
appId: appId,
options: optionsArg is InstanceCreationExpression ? optionsArg : null,
variableToParamName: _variableToParamName,
);
}

/// Extracts Crashlytics alert function declarations.
void _extractCrashlyticsAlertFunction(
MethodInvocation node,
String methodName,
) {
// Map method names to alert types
final alertType = switch (methodName) {
'onNewFatalIssuePublished' => 'crashlytics.newFatalIssue',
'onNewNonfatalIssuePublished' => 'crashlytics.newNonfatalIssue',
'onRegressionAlertPublished' => 'crashlytics.regression',
'onStabilityDigestPublished' => 'crashlytics.stabilityDigest',
'onVelocityAlertPublished' => 'crashlytics.velocity',
'onNewAnrIssuePublished' => 'crashlytics.newAnrIssue',
_ => null,
};

if (alertType == null) return;

_extractAlertEndpoint(node, alertType);
}

/// Extracts Billing alert function declarations.
void _extractBillingAlertFunction(MethodInvocation node, String methodName) {
final alertType = switch (methodName) {
'onPlanUpdatePublished' => 'billing.planUpdate',
'onPlanAutomatedUpdatePublished' => 'billing.planAutomatedUpdate',
_ => null,
};

if (alertType == null) return;

_extractAlertEndpoint(node, alertType);
}

/// Extracts App Distribution alert function declarations.
void _extractAppDistributionAlertFunction(
MethodInvocation node,
String methodName,
) {
final alertType = switch (methodName) {
'onNewTesterIosDevicePublished' => 'appDistribution.newTesterIosDevice',
'onInAppFeedbackPublished' => 'appDistribution.inAppFeedback',
_ => null,
};

if (alertType == null) return;

_extractAlertEndpoint(node, alertType);
}

/// Extracts Performance alert function declarations.
void _extractPerformanceAlertFunction(
MethodInvocation node,
String methodName,
) {
final alertType = switch (methodName) {
'onThresholdAlertPublished' => 'performance.threshold',
_ => null,
};

if (alertType == null) return;

_extractAlertEndpoint(node, alertType);
}

/// Helper to extract alert endpoint from a method invocation.
void _extractAlertEndpoint(MethodInvocation node, String alertType) {
// Extract appId from options if present
final optionsArg = _findNamedArg(node, 'options');
String? appId;

if (optionsArg is InstanceCreationExpression) {
appId = _extractStringField(optionsArg, 'appId');
}

// Generate function name
final sanitizedAlertType =
alertType.replaceAll('.', '_').replaceAll('-', '');
final functionName = 'onAlertPublished_$sanitizedAlertType';

endpoints[functionName] = _EndpointSpec(
name: functionName,
type: 'alert',
alertType: alertType,
appId: appId,
options: optionsArg is InstanceCreationExpression ? optionsArg : null,
variableToParamName: _variableToParamName,
);
}

/// Extracts alert type value from an expression.
String? _extractAlertTypeValue(Expression expression) {
if (expression is InstanceCreationExpression) {
// Extract from constructor: const CrashlyticsNewFatalIssue()
final typeName = expression.constructorName.type.name2.lexeme;
return switch (typeName) {
'CrashlyticsNewFatalIssue' => 'crashlytics.newFatalIssue',
'CrashlyticsNewNonfatalIssue' => 'crashlytics.newNonfatalIssue',
'CrashlyticsRegression' => 'crashlytics.regression',
'CrashlyticsStabilityDigest' => 'crashlytics.stabilityDigest',
'CrashlyticsVelocity' => 'crashlytics.velocity',
'CrashlyticsNewAnrIssue' => 'crashlytics.newAnrIssue',
'BillingPlanUpdate' => 'billing.planUpdate',
'BillingPlanAutomatedUpdate' => 'billing.planAutomatedUpdate',
'AppDistributionNewTesterIosDevice' =>
'appDistribution.newTesterIosDevice',
'AppDistributionInAppFeedback' => 'appDistribution.inAppFeedback',
'PerformanceThreshold' => 'performance.threshold',
_ => null,
};
}
return null;
}

/// Extracts a parameter definition from FunctionExpressionInvocation.
void _extractParameter(
FunctionExpressionInvocation node,
Expand Down Expand Up @@ -598,11 +814,14 @@ class _EndpointSpec {
this.databaseEventType,
this.refPath,
this.instance,
this.alertType,
this.appId,
this.options,
this.variableToParamName = const {},
});
final String name;
final String type; // 'https', 'callable', 'pubsub', 'firestore', 'database'
// 'https', 'callable', 'pubsub', 'firestore', 'database', 'alert'
final String type;
final String? topic; // For Pub/Sub functions
final String? firestoreEventType; // For Firestore: onDocumentCreated, etc.
final String? documentPath; // For Firestore: users/{userId}
Expand All @@ -611,6 +830,8 @@ class _EndpointSpec {
final String? databaseEventType; // For Database: onValueCreated, etc.
final String? refPath; // For Database: /users/{userId}
final String? instance; // For Database: database instance or '*'
final String? alertType; // For Alerts: crashlytics.newFatalIssue, etc.
final String? appId; // For Alerts: optional app ID filter
final InstanceCreationExpression? options;
final Map<String, String> variableToParamName;

Expand Down Expand Up @@ -1345,6 +1566,17 @@ String _generateYaml(
buffer.writeln(' ref: "$normalizedRef"');
buffer.writeln(' instance: "${endpoint.instance ?? '*'}"');

buffer.writeln(' retry: false');
} else if (endpoint.type == 'alert' && endpoint.alertType != null) {
buffer.writeln(' eventTrigger:');
buffer.writeln(
' eventType: "google.firebase.firebasealerts.alerts.v1.published"',
);
buffer.writeln(' eventFilters:');
buffer.writeln(' alerttype: "${endpoint.alertType}"');
if (endpoint.appId != null) {
buffer.writeln(' appid: "${endpoint.appId}"');
}
buffer.writeln(' retry: false');
}
}
Expand Down
2 changes: 2 additions & 0 deletions lib/firebase_functions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export 'package:shelf/shelf.dart' show Request, Response;

// Re-export built-in params from params.dart for convenience
export 'params.dart' show databaseURL, gcloudProject, projectID, storageBucket;
// Alerts triggers
export 'src/alerts/alerts.dart';
// Common types
export 'src/common/cloud_event.dart';
export 'src/common/expression.dart';
Expand Down
Loading
Loading