Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: update app event filter example to be a single frontendless app [EXT-6286] #9602

Merged
merged 1 commit into from
Mar 25, 2025
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
13 changes: 13 additions & 0 deletions examples/function-appevent-filter/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# These environment variables are required to upload your function to Contentful
CONTENTFUL_ACCESS_TOKEN=access-token
CONTENTFUL_ORG_ID=org-id
CONTENTFUL_APP_DEF_ID=app-def-id

# These additional environment variables are required to run the `npm run create-app-event` and `npm run `create-content-type-publish-entries` scripts
CONTENTFUL_SPACE_ID=space-id
# The environment ID is optional. If not provided, the default environment (master) will be used.
CONTENTFUL_ENVIRONMENT_ID=master
# The host is optional. If not provided, the default host (api.contentful.com) will be used.
CONTENTFUL_HOST=api.contentful.com
CONTENTFUL_FUNCTION_ID=appeventFilterExample
CONTENTFUL_APP_EVENT_TARGET_URL=https://webhook.site/sample
3 changes: 3 additions & 0 deletions examples/function-appevent-filter/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
build/
.env
1 change: 1 addition & 0 deletions examples/function-appevent-filter/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v22.14.0
146 changes: 146 additions & 0 deletions examples/function-appevent-filter/INSTRUCTIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# App Event Filter Function Example

App event filter functions enable you to decide which events your app should process by inspecting event payloads before they reach your main logic. This example demonstrates how you can use a function to filter entry publish events based on the sentiment of the entry's description field.

## What is an App Event Filter Function?

App event filter functions give you fine-grained control over event processing by letting you analyze the event content before deciding whether to process it.

Common use cases for filtering events include:

- Only processing events from specific content types
- Filtering based on field values or metadata
- Reducing event processing load by filtering out unnecessary events

## Getting Started

This example app demonstrates how to use a filter function to filter entry publish events based on the sentiment of a description field on the entry. If an entry has positive content in the description field, the function will evaluate to `true` and the event will be sent through. If an entry has negative content in the description field, the function will evaluate to `false` and the event will not be sent through.

### 1. Install the Example App

Use this command below to install the example:

```bash
npx create-contentful-app@latest --example function-appevent-filter
```

### 2. Connect to an App Definition

If you haven't already created an app definition in Contentful, choose one of the options below.

#### Manually via the Web UI

- [Navigate to the Apps section in your organization](https://app.contentful.com/deeplink?link=app-definition-list)

- Click the "Create App" button

- Fill in the required fields. **Note:** This app is **frontendless**, so an app location does not need to be selected.

- Proceed to the [Set Up Your Environment](#3-set-up-your-environment) step.

> **Note**: If you are unfamiliar with how to create a custom app definition in Contentful, please review the documentation here: [Create a Custom App - Tutorial](https://www.contentful.com/developers/docs/extensibility/app-framework/tutorial/?utm_source=webapp&utm_medium=app-listing&utm_campaign=in-app-help)

#### Via CLI

- Run: `npm run create-app-definition`
- Answer the prompted questions. Feel free to proceed with the default options provided.

1. **Name of your application**.
- This is how your app will be named and it will be displayed in a few places throughout the UI. The default is the name of the folder you created.
2. This app is **frontendless**, so an app location does not need to be selected.
3. **Contentful CMA endpoint URL**.
- This refers to the URL used to interact with Contentful's Management APIs.
4. No **app parameters** are needed for this app.
5. The next steps will lead you through the process of providing a Contentful access token to the application and specifying the organization to which the application should be assigned.
- This will automatically create a `.env` file with these fields for you
6. Proceed to [Customize the Filter Function](#4-examine-the-filter-function-code)

### 3. Set Up Your Environment

If you created your app definition manually through the web UI, or the CLI did not create one for you, create a `.env` file in the root of your application with your Contentful credentials:

```env
CONTENTFUL_ORG_ID=your-organization-id
CONTENTFUL_APP_DEF_ID=your-app-definition-id
CONTENTFUL_ACCESS_TOKEN=your-access-token
```

These variables authenticate your function with Contentful and link it to your app definition.

> **Note**: You can generate an access token from your Space Settings menu. For the other values, you can find them in your Contentful organization and app settings.

### 4. Examine the Filter Function Code

Open `functions/appevent-filter-example.ts` to see how this function will filter events.

The function uses a package called `sentiment` to analyze the content of a description field on an entry. If the sentiment score is zero or above, then the function will return true and the event will continue. If it is below zero, then the function will return false and the event will be discarded.

### 5. Build and Upload Your Function

Currently, the only way to deploy a function is through the CLI. To do so, run the following commands:

```bash
# Build your function
npm run build

# Upload your function to Contentful
npm run upload
```

The build step is essential since the upload process relies on the compiled code. The CLI may prompt for additional details (e.g., the CMA endpoint URL) and offer to activate the bundle post-upload.

> **Note**: For more information on the differences between `upload` and `upload-ci`, see the [Create Contentful App Documentation](https://www.contentful.com/developers/docs/extensibility/app-framework/create-contentful-app/)

### 6. Link Your Function to an Event Subscription

To activate your function, you need to subscribe it to specific events. Before setting up your app event subscription, you will need a URL endpoint for the event target. For local testing, we recommend to use [webhook.site](https://webhook.site/) to inspect the sent events.

There are a few ways to do this:

#### Run the Provided Script

This example app comes with a script that creates an app event subscription using the CMA JavaScript SDK. Before using the script, ensure that you have created a `.env` file with all of the environment variables listed in the `.env.example` file. Then run:

```bash
npm run create-app-event
```

This will create an app event subscription with the target URL you specified, the filter function you uploaded to your app, and a subscription to the entry publish event.

#### Via the Contentful Web App

1. Open your app definition in the Contentful web app
2. Navigate to the "Events" tab
3. Select the option to "Enable events"
4. Create an event subscription

- For the "Target", select "Target a URL" and paste the generated URL from `webhook.site` into the "Target a URL" input field.
- In the "Filter Function" section, this is where you select your uploaded filter function. When this is selected, all events will be processed by this filter function. Any events that return false will be discarded and will not continue to the target. Only events returning true will continue to the target.
- For the "Topics", select the Entry Publish event

### 7. Install the App

Next, you will need to install the app into a space. Run this script `npm run install-app` and then select a space and environment for where you would like to install the app.

### 8. Create a Content Type and Publish an Entry

The final step is to create a content type that has a description field for the filter function to run a sentiment analysis on, and to create and publish entries. The easiest way to do this is to use the provided script. Before using the script, ensure that you have created a `.env` file with all of the environment variables listed in the `.env.example` file. Ensure that the space and environment you add to your environment variables match where you have installed the app.

Then run:

```bash
npm run create-content-type-publish-entries
```

This script uses the CMA JavaScript SDK to create a content type, publish the content type, create two entries (one positive and one negative), and publish those two entries.

### 9. Inspect the sent events and function logs

After running the script, you can inspect the sent events and function logs to see that the filter function worked correctly. You should have only received one event to your URL endpoint for when the positive entry published. Navigate to your app definition (run `npm run open-settings`), click on the "Functions" tab, then the menu next to your function, and click "View Logs". Select your space and environment to see the logs for your function. You should see two logs, one for each published entry. Clicking on each log will show the sentiment score for each entry; the positive one will have a positive score and the negative one will have a negative score (which is why that event was filtered and not sent).

## Additional Resources

- [Contentful App Functions Documentation](https://www.contentful.com/developers/docs/extensibility/app-framework/functions/)
- [Working with Functions](https://www.contentful.com/developers/docs/extensibility/app-framework/working-with-functions/)
- [App Event Subscriptions API Reference](https://www.contentful.com/developers/docs/references/content-management-api/#/reference/app-event-subscriptions)
- [App Events Overview](https://www.contentful.com/developers/docs/extensibility/app-framework/app-events/)
13 changes: 13 additions & 0 deletions examples/function-appevent-filter/contentful-app-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"functions": [
{
"id": "appeventFilterExample",
"name": "Example App Event Filter Function",
"description": "This is an example to help you learn how App Event filter functions work; this function filters entry publish events based on the sentiment of the entry's description field.",
"path": "functions/appevent-filter-example.js",
"entryFile": "functions/appevent-filter-example.ts",
"allowNetworks": [],
"accepts": ["appevent.filter"]
}
]
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { FunctionEventHandler } from '@contentful/node-apps-toolkit';
import { AppEventEntry } from '@contentful/node-apps-toolkit/lib/requests/typings';
import {
FunctionTypeEnum,
EntryCreateEventPayload,
} from '@contentful/node-apps-toolkit/lib/requests/typings';

// Import and instantiate a sentiment analysis library
import Sentiment from 'sentiment';
const sentiment = new Sentiment();

// Since our function only accepts filter events,
// we can safely assume the event is of type appevent.filter
export const handler: FunctionEventHandler<'appevent.filter'> = (event, context) => {
export const handler: FunctionEventHandler<FunctionTypeEnum.AppEventFilter> = (event, context) => {
// Since our app event subscription only reacts to Entry publish events,
// we can safely assume that the event is an AppEventEntry
const { body } = event as AppEventEntry;
// we can safely assume that the event body is EntryCreateEventPayload
const { body } = event as { body: EntryCreateEventPayload };

// Extract the 'description' field from the body and analyze its sentiment
const description = body.fields.description['en-US'];
Expand Down
34 changes: 0 additions & 34 deletions examples/function-appevent-filter/javascript/INSTRUCTIONS.md

This file was deleted.

This file was deleted.

14 changes: 0 additions & 14 deletions examples/function-appevent-filter/javascript/example.js

This file was deleted.

9 changes: 0 additions & 9 deletions examples/function-appevent-filter/javascript/package.json

This file was deleted.

27 changes: 27 additions & 0 deletions examples/function-appevent-filter/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "function-appevent-filter",
"version": "0.1.0",
"scripts": {
"build": "npm run build:functions",
"build:functions": "contentful-app-scripts build-functions --ci",
"create-app-definition": "contentful-app-scripts create-app-definition",
"add-locations": "contentful-app-scripts add-locations",
"upload": "contentful-app-scripts upload --bundle-dir ./build",
"upload-ci": "contentful-app-scripts upload --ci --bundle-dir ./build --organization-id $CONTENTFUL_ORG_ID --definition-id $CONTENTFUL_APP_DEF_ID --token $CONTENTFUL_ACCESS_TOKEN",
"open-settings": "contentful-app-scripts open-settings",
"install-app": "contentful-app-scripts install",
"create-app-event": "tsx -r dotenv/config ./src/tools/create-app-event.ts",
"create-content-type-publish-entries": "tsx -r dotenv/config ./src/tools/create-content-type-publish-entries.ts"
},
"devDependencies": {
"@contentful/app-scripts": "^2.3.0",
"@contentful/node-apps-toolkit": "^3.12.0",
"@tsconfig/recommended": "1.0.8",
"@types/node": "^16.18.126",
"@types/sentiment": "^5.0.4",
"contentful-management": "^11.48.3",
"sentiment": "^5.0.2",
"tsx": "^4.19.3",
"typescript": "^5.8.2"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import assert from 'node:assert';
import { createClient } from 'contentful-management';

const {
CONTENTFUL_ORG_ID: organizationId = '',
CONTENTFUL_APP_DEF_ID: appDefinitionId = '',
CONTENTFUL_ACCESS_TOKEN: accessToken = '',
CONTENTFUL_HOST: contentfulHost = 'api.contentful.com',
CONTENTFUL_SPACE_ID: spaceId = '',
CONTENTFUL_ENVIRONMENT_ID: environmentId = 'master',
CONTENTFUL_FUNCTION_ID: functionId = '',
CONTENTFUL_APP_EVENT_TARGET_URL: targetUrl = '',
} = process.env;

assert.ok(organizationId !== '', `CONTENTFUL_ORG_ID environment variable must be defined`);

assert.ok(appDefinitionId !== '', `CONTENTFUL_APP_DEF_ID environment variable must be defined`);

assert.ok(accessToken !== '', `CONTENTFUL_ACCESS_TOKEN environment variable must be defined`);

assert.ok(spaceId !== '', `CONTENTFUL_SPACE_ID environment variable must be defined`);

assert.ok(functionId !== '', `CONTENTFUL_FUNCTION_ID environment variable must be defined`);

assert.ok(targetUrl !== '', `CONTENTFUL_APP_EVENT_TARGET_URL environment variable must be defined`);

const client = createClient(
{
accessToken,
host: contentfulHost,
},
{ type: 'plain' }
);

export {
client,
organizationId,
appDefinitionId,
accessToken,
contentfulHost,
spaceId,
environmentId,
functionId,
targetUrl,
};
38 changes: 38 additions & 0 deletions examples/function-appevent-filter/src/tools/create-app-event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {
client,
organizationId,
appDefinitionId,
targetUrl,
functionId,
} from './contentful-client-and-imports';

const createAppEventSubscription = async () => {
try {
const eventSubscription = await client.appEventSubscription.upsert(
{
organizationId,
appDefinitionId,
},
{
targetUrl,
topics: ['Entry.publish'],
functions: {
filter: {
sys: {
type: 'Link',
linkType: 'Function',
id: functionId,
},
},
},
}
);

console.log('Subscription created');
console.dir(eventSubscription, { depth: 5 });
} catch (error) {
console.error(error);
}
};

createAppEventSubscription();
Loading