Skip to content

Commit

Permalink
Add newsletter sign up serverless function.
Browse files Browse the repository at this point in the history
  • Loading branch information
eugenchio committed Jan 10, 2025
1 parent 93c8a6b commit 88aa178
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 97 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ jobs:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
PIPE_DRIVE_API_TOKEN: ${{ secrets.PIPE_DRIVE_API_TOKEN }}
PIPE_DRIVE_LEAD_CUSTOM_DATA: ${{ secrets.PIPE_DRIVE_LEAD_CUSTOM_DATA }}
CRISP_TOKEN_ID: ${{ secrets.CRISP_TOKEN_ID }}
CRISP_TOKEN_KEY: ${{ secrets.CRISP_TOKEN_KEY }}
run: uv run run.py deploy-lambda

# Send notification on build or deploy failure
Expand Down
159 changes: 106 additions & 53 deletions cloudformation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ Resources:
- "ses:SendRawEmail"
Resource: "*"

ContactFormLambda:
LandingContactFormLambda:
Type: "AWS::Lambda::Function"
Properties:
Handler: "contact_form.lambda_handler"
Expand All @@ -152,10 +152,10 @@ Resources:
Runtime: "python3.9"
Timeout: 10

LandingContactFormLambda:
LandingNewsletterSignUpLambda:
Type: "AWS::Lambda::Function"
Properties:
Handler: "contact_form.lambda_handler"
Handler: "newsletter_sign_up.lambda_handler"
Role: !GetAtt LambdaExecutionRole.Arn
Code:
# use CF feature - it compares yaml config with its previous version
Expand Down Expand Up @@ -185,19 +185,6 @@ Resources:
Properties:
CloudWatchRoleArn: !GetAtt ApiGatewayLoggingRole.Arn

ContactFormAPIGateway:
Type: 'AWS::ApiGateway::RestApi'
DependsOn:
- ContactFormLambda
- ApiGatewayLoggingRole
Properties:
Name: ContactFormAPIGateway
FailOnWarnings: 'true'
Description: 'Contact Form API Gateway'
EndpointConfiguration:
Types:
- REGIONAL

LandingAPIGateway:
Type: 'AWS::ApiGateway::RestApi'
DependsOn:
Expand All @@ -211,19 +198,19 @@ Resources:
Types:
- REGIONAL

ContactFormAPIMethod:
LandingContactFormAPIMethod:
Type: 'AWS::ApiGateway::Method'
Properties:
AuthorizationType: NONE
HttpMethod: ANY
ResourceId: !GetAtt ContactFormAPIGateway.RootResourceId
RestApiId: !Ref ContactFormAPIGateway
ResourceId: !GetAtt LandingAPIGateway.RootResourceId
RestApiId: !Ref LandingAPIGateway
Integration:
IntegrationHttpMethod: POST
Type: AWS
Uri: !Sub
- arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaArn}/invocations
- LambdaArn: !GetAtt ContactFormLambda.Arn
- LambdaArn: !GetAtt LandingContactFormLambda.Arn
IntegrationResponses:
- StatusCode: 200
ResponseParameters:
Expand All @@ -245,13 +232,13 @@ Resources:
RequestModels:
application/json: Empty

ContactFormAPIOptionsMethod:
LandingContactFormAPIOptionsMethod:
Type: 'AWS::ApiGateway::Method'
Properties:
AuthorizationType: NONE
HttpMethod: OPTIONS
ResourceId: !GetAtt ContactFormAPIGateway.RootResourceId
RestApiId: !Ref ContactFormAPIGateway
ResourceId: !GetAtt LandingAPIGateway.RootResourceId
RestApiId: !Ref LandingAPIGateway
Integration:
IntegrationResponses:
- StatusCode: 200
Expand All @@ -272,12 +259,19 @@ Resources:
method.response.header.Access-Control-Allow-Methods: true
method.response.header.Access-Control-Allow-Headers: true

LandingContactFormAPIMethod:
LandingContactFormAPIResource:
Type: AWS::ApiGateway::Resource
Properties:
ParentId: !GetAtt LandingAPIGateway.RootResourceId
PathPart: 'contact'
RestApiId: !Ref LandingAPIGateway

LandingNewContactFormAPIMethod:
Type: 'AWS::ApiGateway::Method'
Properties:
AuthorizationType: NONE
HttpMethod: ANY
ResourceId: !GetAtt LandingAPIGateway.RootResourceId
ResourceId: !Ref LandingContactFormAPIResource
RestApiId: !Ref LandingAPIGateway
Integration:
IntegrationHttpMethod: POST
Expand Down Expand Up @@ -306,12 +300,12 @@ Resources:
RequestModels:
application/json: Empty

LandingContactFormAPIOptionsMethod:
LandingNewContactFormAPIOptionsMethod:
Type: 'AWS::ApiGateway::Method'
Properties:
AuthorizationType: NONE
HttpMethod: OPTIONS
ResourceId: !GetAtt LandingAPIGateway.RootResourceId
ResourceId: !Ref LandingContactFormAPIResource
RestApiId: !Ref LandingAPIGateway
Integration:
IntegrationResponses:
Expand All @@ -333,33 +327,83 @@ Resources:
method.response.header.Access-Control-Allow-Methods: true
method.response.header.Access-Control-Allow-Headers: true

ContactFormAPIDeployment:
Type: 'AWS::ApiGateway::Deployment'
DependsOn:
- ContactFormAPIMethod
LandingNewsletterSignUpAPIResource:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId: !Ref ContactFormAPIGateway
ParentId: !GetAtt LandingAPIGateway.RootResourceId
PathPart: 'newsletter'
RestApiId: !Ref LandingAPIGateway

LandingNewsletterSignUpAPIMethod:
Type: 'AWS::ApiGateway::Method'
Properties:
AuthorizationType: NONE
HttpMethod: ANY
ResourceId: !Ref LandingNewsletterSignUpAPIResource
RestApiId: !Ref LandingAPIGateway
Integration:
IntegrationHttpMethod: POST
Type: AWS
Uri: !Sub
- arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaArn}/invocations
- LambdaArn: !GetAtt LandingNewsletterSignUpLambda.Arn
IntegrationResponses:
- StatusCode: 200
ResponseParameters:
method.response.header.Access-Control-Allow-Origin: !Sub "'${CorsAllowedOrigins}'"
method.response.header.Access-Control-Allow-Methods: "'*'"
method.response.header.Access-Control-Allow-Headers: "'*'"
ResponseTemplates:
application/json: '{"status":"ok"}'
RequestParameters:
integration.request.header.X-Amz-Invocation-Type: "'Event'"
MethodResponses:
- StatusCode: 200
ResponseParameters:
method.response.header.Access-Control-Allow-Origin: true
method.response.header.Access-Control-Allow-Methods: true
method.response.header.Access-Control-Allow-Headers: true
RequestParameters:
method.request.header.Content-Type: false
RequestModels:
application/json: Empty

LandingNewsletterSignUpAPIOptionsMethod:
Type: 'AWS::ApiGateway::Method'
Properties:
AuthorizationType: NONE
HttpMethod: OPTIONS
ResourceId: !Ref LandingNewsletterSignUpAPIResource
RestApiId: !Ref LandingAPIGateway
Integration:
IntegrationResponses:
- StatusCode: 200
ResponseParameters:
method.response.header.Access-Control-Allow-Origin: !Sub "'${CorsAllowedOrigins}'"
method.response.header.Access-Control-Allow-Methods: "'*'"
method.response.header.Access-Control-Allow-Headers: "'*'"
ResponseTemplates:
application/json: Empty
PassthroughBehavior: WHEN_NO_MATCH
RequestTemplates:
application/json: '{"statusCode": 200}'
Type: MOCK
MethodResponses:
- StatusCode: 200
ResponseParameters:
method.response.header.Access-Control-Allow-Origin: true
method.response.header.Access-Control-Allow-Methods: true
method.response.header.Access-Control-Allow-Headers: true

LandingAPIDeployment:
Type: 'AWS::ApiGateway::Deployment'
DependsOn:
- LandingContactFormAPIMethod
- LandingNewContactFormAPIMethod
- LandingNewsletterSignUpAPIMethod
Properties:
RestApiId: !Ref LandingAPIGateway

# Enable logging of all HTTP requests
ContactFormAPIStage:
Type: AWS::ApiGateway::Stage
Properties:
DeploymentId: !Ref ContactFormAPIDeployment
MethodSettings:
- HttpMethod: '*'
LoggingLevel: INFO
ResourcePath: /*
DataTraceEnabled: true
RestApiId: !Ref ContactFormAPIGateway
StageName: !Ref apiGatewayStageName

# Enable logging of all HTTP requests
LandingAPIStage:
Type: AWS::ApiGateway::Stage
Expand All @@ -374,27 +418,36 @@ Resources:
StageName: !Ref apiGatewayStageName

# Create permission for API Gateway to invoke Lambda
ContactFormAPIPermission:
LandingAPIPermission:
Type: 'AWS::Lambda::Permission'
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref ContactFormLambda
FunctionName: !Ref LandingContactFormLambda
Principal: apigateway.amazonaws.com
SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ContactFormAPIGateway}/*/*/'
SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${LandingAPIGateway}/*/*/'

# Create permission for API Gateway to invoke Lambda
LandingAPIPermission:
LandingContactFormAPIPermission:
Type: 'AWS::Lambda::Permission'
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref LandingContactFormLambda
Principal: apigateway.amazonaws.com
SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${LandingAPIGateway}/*/*/'

# Create permission for API Gateway to invoke Lambda
LandingNewsletterSignUpAPIPermission:
Type: 'AWS::Lambda::Permission'
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref LandingNewsletterSignUpLambda
Principal: apigateway.amazonaws.com
SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${LandingAPIGateway}/*/*/'

Outputs:
ContactFormInvokeURL:
Value: !Sub 'https://${ContactFormAPIGateway}.execute-api.${AWS::Region}.amazonaws.com/${apiGatewayStageName}/'
Description: URL for invoking the Contact Form API
LandingContactFormInvokeURL:
Value: !Sub 'https://${LandingAPIGateway}.execute-api.${AWS::Region}.amazonaws.com/${apiGatewayStageName}/'
Value: !Sub 'https://${LandingAPIGateway}.execute-api.${AWS::Region}.amazonaws.com/${apiGatewayStageName}/contact'
Description: URL for invoking the Contact Form API
LandingNewsletterSignUpInvokeURL:
Value: !Sub 'https://${LandingAPIGateway}.execute-api.${AWS::Region}.amazonaws.com/${apiGatewayStageName}/newsletter'
Description: URL for invoking the Contact Form API
8 changes: 8 additions & 0 deletions deploy/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,11 @@

CONTACT_FORM_FROM_EMAIL = '[email protected]'
CONTACT_FORM_TO_EMAIL = '[email protected]'

# WARNING: Crisp Development Token is connected to the PRODUCTION environment,
# since Crisp has no concept of test environment, so be careful.
#
# Development Token is limited to 500/requests day.
# If needed you can reset the limit here (see login credentials in Notion):
# https://marketplace.crisp.chat/plugins/plugin/421c7588-7ed5-464a-a1f7-8917ae9bafe7/tokens/
CRISP_WEBSITE_ID = '6de2ec98-888b-4ff4-b9be-dab91a460ee6'
65 changes: 65 additions & 0 deletions lambda-functions/newsletter_sign_up.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import json
import logging
import os
from json import JSONDecodeError

import boto3
import pipedrive
import sentry_sdk
from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
from crisp_api import Crisp


logger = logging.getLogger()
logger.setLevel(logging.INFO)


sentry_sdk.init(
dsn="https://[email protected]"
+ "/4506218347954176",
integrations=[AwsLambdaIntegration()],
traces_sample_rate=1.0,
profiles_sample_rate=1.0,
)


def add_crisp_contact(event_body):
logger.info('add_crisp_contact(): start')
crisp = Crisp()
crisp.set_tier('plugin')
website_id = os.environ['CRISP_WEBSITE_ID']
token_id = os.environ['CRISP_TOKEN_ID']
token_key = os.environ['CRISP_TOKEN_KEY']

if not (website_id and token_id and token_key):
warnings.warn('Crisp credentials are missing!')
return

crisp.authenticate(token_id, token_key)

try:
contact_data = crisp.website.add_new_people_profile(
website_id=website_id,
data={
'email': event_body['email'],
'person': {'nickname': event_body['email']},
'segments': 'newsletter',
},
)
except Exception as e:
sentry_sdk.capture_exception(e)
logger.exception('add_crisp_contact(): add_crisp_contact failed')
return

logger.info('add_crisp_contact(): success')


def lambda_handler(event):
assert event.get('email') is not None

logger.info('lambda_handler(): invoked. event={}'.format(event))
try:
add_crisp_contact(event)
except Exception as e:
sentry_sdk.capture_exception(e)
logger.exception('lambda_handler(): add_crisp_contact failed')
1 change: 1 addition & 0 deletions lambda-functions/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ dependencies = [
"boto3==1.29.1",
"requests==2.31.0",
"sentry-sdk==1.35.0",
"crisp-api==1.1.19",
]
Loading

0 comments on commit 88aa178

Please sign in to comment.