Skip to content
Open
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
31 changes: 31 additions & 0 deletions Python/app-distribution-feedback-to-jira/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Send in-app feedback to Jira

This [code](functions/main.py) demonstrates how to use a Firebase Cloud Function triggered by an
[in-app feedback Firebase Alert from App Distribution](https://firebase.google.com/docs/functions/beta/reference/firebase-functions.alerts.appdistribution.inappfeedbackpayload),
and stores the feedback details (including screenshot) in Jira.

You can customize this code to work with your own Jira configuration (eg on-premise support, custom issue types, etc).

## Quickstart

This sample code uses Jira's built-in APIs to create issues for in-app tester feedback. For simplicity it uses [basic authorization](https://developer.atlassian.com/cloud/jira/platform/basic-auth-for-rest-apis/).

1. [Generate an API token](https://id.atlassian.com/manage-profile/security/api-tokens) via your Jira profile.


Note: If the tester who files feedback does not have a Jira account, the user who generates this token will be marked as the issue's reporter.
2. This [code](functions/main.py) uses [parameterized configuration](https://firebase.google.com/docs/functions/config-env#params) to prompt for the required configuratio. To start the process, run:
```bash
$ firebase deploy
```
This will store the `API_TOKEN` using [Google Cloud Secret Manager](https://cloud.google.com/secret-manager) and the remaining settings in an `.env` file which will contain the following variables, customized to your Jira project:
```bash
JIRA_URI="<your JIRA instance's URI, e.g. 'https://mysite.atlassian.net'>"
PROJECT_KEY="<your project's key, e.g. 'DEV'>"
ISSUE_TYPE_ID="<issue type ID; defaults to '10001' (Improvement)>"
ISSUE_LABEL="<label applied to the Jira issues created; defaults to 'in-app'>"
API_TOKEN_OWNER="<creator of the token; default reporter of issues>"
```

## License
© Google, 2022. Licensed under an [Apache-2 license](../../LICENSE).
17 changes: 17 additions & 0 deletions Python/app-distribution-feedback-to-jira/firebase.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"functions": [
{
"source": "functions",
"codebase": "app-distribution-feedback-to-jira",
"ignore": [
"venv",
".git",
"firebase-debug.log",
"firebase-debug.*.log"
],
"predeploy": [
"npm --prefix \"$RESOURCE_DIR\" run lint"
]
}
]
}
273 changes: 273 additions & 0 deletions Python/app-distribution-feedback-to-jira/functions/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import requests
import base64
from firebase_functions import options
from firebase_functions.alerts.app_distribution_fn import (
on_in_app_feedback_published,
InAppFeedbackEvent,
)
from firebase_functions.params import (
StringParam,
IntParam,
SecretParam,
)

# The keys are either defined in .env or they are created
# via prompt in the CLI before deploying
JIRA_URI = StringParam(
"JIRA_URI",
description="URI of your Jira instance (e.g. 'https://mysite.atlassian.net')",
input={
"text": {
"validation_regex": r"^https://.*",
"validation_error_message": "Please enter an 'https://' URI",
}
},
)
PROJECT_KEY = StringParam("PROJECT_KEY",
description="Project key of your Jira instance (e.g. 'XY')")
ISSUE_TYPE_ID = IntParam(
"ISSUE_TYPE_ID",
description="Issue type ID for the Jira issues being created",
default=10001,
)
ISSUE_LABEL = StringParam(
"ISSUE_LABEL",
description="Label for the Jira issues being created",
default="in-app",
)
API_TOKEN_OWNER = StringParam(
"API_TOKEN_OWNER",
description="Owner of the Jira API token",
input={
"text": {
"validation_regex":
r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$",
"validation_error_message":
"Please enter a valid email address",
}
},
)
API_TOKEN = SecretParam(
"API_TOKEN",
description="Jira API token. Created using "
"https://id.atlassian.com/manage-profile/security/api-tokens",
)


@on_in_app_feedback_published(secrets=[API_TOKEN])
def handle_in_app_feedback(event: InAppFeedbackEvent):
issue_uri = create_issue(event)
if event.data.payload.screenshot_uri:
upload_screenshot(issue_uri, event.data.payload.screenshot_uri)


def auth_header():
"""Creates "Authorization" header value."""
token = f"{API_TOKEN_OWNER.value()}:{API_TOKEN.value()}"
return "Basic " + base64.b64encode(token.encode("utf-8")).decode("utf-8")


def create_issue(event: InAppFeedbackEvent):
"""Creates new issue in Jira."""
request_json = build_create_issue_request(event)
response = requests.post(
f"{JIRA_URI.value()}/rest/api/3/issue",
headers={
"Authorization": auth_header(),
"Accept": "application/json",
"Content-Type": "application/json",
},
json=request_json,
)
if not response.ok:
raise Exception(
f"Issue creation failed: {response.status_code} {response.reason} for {request_json}")
return response.json()["self"] # issueUri


def upload_screenshot(issue_uri: str, screenshot_uri: str):
"""Uploads screenshot to Jira (after downloading it from Firebase)."""
dl_response = requests.get(screenshot_uri)
if not dl_response.ok:
raise Exception(
f"Screenshot download failed: {dl_response.status_code} {dl_response.reason}")
blob = dl_response.content
files = {"file": ("screenshot.png", blob, "image/png")}
ul_response = requests.post(
f"{issue_uri}/attachments",
headers={
"Authorization": auth_header(),
"Accept": "application/json",
"X-Atlassian-Token": "no-check",
},
files=files,
)
if not ul_response.ok:
raise Exception(f"Screenshot upload failed: {ul_response.status_code} {ul_response.reason}")


def lookup_reporter(tester_email: str):
"""Looks up Jira user ID."""
response = requests.get(
f"{JIRA_URI.value()}/rest/api/3/user/search?query={tester_email}",
headers={
"Authorization": auth_header(),
"Accept": "application/json"
},
)
if not response.ok:
print(
f"Failed to find Jira user for '{tester_email}': {response.status_code} {response.reason}"
)
return None
json = response.json()
return json[0]["accountId"] if len(json) > 0 else None


def build_create_issue_request(event: InAppFeedbackEvent):
"""Builds payload for creating a Jira issue."""
summary = "In-app feedback: " + event.data.payload.text
summary = summary.splitlines()[0]
if len(summary) > 40:
summary = summary[:39] + "…"
json = {
"update": {},
"fields": {
"summary": summary,
"issuetype": {
"id": str(ISSUE_TYPE_ID.value())
},
"project": {
"key": PROJECT_KEY.value()
},
"description": {
"type":
"doc",
"version":
1,
"content": [
{
"type":
"paragraph",
"content": [
{
"text": "Firebase App ID: ",
"type": "text",
"marks": [{
"type": "strong"
}],
},
{
"text": event.app_id,
"type": "text"
},
],
},
{
"type":
"paragraph",
"content": [
{
"text": "App Version: ",
"type": "text",
"marks": [{
"type": "strong"
}],
},
{
"text": event.data.payload.app_version,
"type": "text"
},
],
},
{
"type":
"paragraph",
"content": [
{
"text": "Tester Email: ",
"type": "text",
"marks": [{
"type": "strong"
}],
},
{
"text": event.data.payload.tester_email,
"type": "text"
},
],
},
{
"type":
"paragraph",
"content": [
{
"text": "Tester Name: ",
"type": "text",
"marks": [{
"type": "strong"
}],
},
{
"text": event.data.payload.tester_name or "None",
"type": "text",
},
],
},
{
"type":
"paragraph",
"content": [
{
"text": "Feedback text: ",
"type": "text",
"marks": [{
"type": "strong"
}],
},
{
"text": event.data.payload.text,
"type": "text"
},
],
},
{
"type":
"paragraph",
"content": [{
"text":
"Console link",
"type":
"text",
"marks": [{
"type": "link",
"attrs": {
"href": event.data.payload.feedback_console_uri,
"title": "Firebase console",
},
}],
}],
},
],
},
"labels": [ISSUE_LABEL.value()],
},
}
reporter = lookup_reporter(event.data.payload.tester_email)
if reporter:
json["fields"]["reporter"] = {"id": reporter}
return json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
firebase-functions
requests
Loading