AppStore Reviews Bot|Automate APP Review Tracking with Slack Notifications
Develop an AppStore review tracking bot that sends real-time Slack alerts, solving manual monitoring delays and boosting your app's responsiveness to user feedback effectively.
点击这里查看本文章简体中文版本。
點擊這裡查看本文章正體中文版本。
This post was translated with AI assistance — let me know if anything sounds off!
AppStore APP’s Reviews Slack Bot Stories
Using Ruby + Fastlane-SpaceShip to Build an App Review Tracking Notification Slack Bot
Photo by Austin Distel
Eating Rice Without Knowing Its Price
I recently found out that the bot forwarding the latest app review messages in Slack requires a fee. I always thought this feature was free. The cost ranges from $5 to $200 per month because each platform offers more than just the “App Review Bot” function. They also provide data analytics, records, unified dashboards, competitor comparisons, and more. The price depends on the services each platform offers. The Review Bot is just one part of their service, but I only want this feature and nothing else. In that case, paying seems quite wasteful.
Question
Originally, we used the free open-source tool TradeMe/ReviewMe for Slack notifications, but this tool has been unmaintained for years. Occasionally, Slack floods with old reviews, which is alarming (many bugs have long been fixed, making us think there was a new issue!). The cause is unknown.
So consider finding other tools or methods as replacements.
TL;DR [2022/08/10] Update:
The App Reviews Bot has been redesigned using the new App Store Connect API and relaunched as “ZReviewTender — Free and Open Source App Reviews Monitoring Bot”.
====
2022/07/20 Update
App Store Connect API now supports reading and managing Customer Reviews. The App Store Connect API natively supports accessing app reviews, no longer requiring Fastlane — Spaceship to fetch reviews from the backend.
Principle Exploration
After having the motivation, next study the principles of achieving the goal.
Official API ❌
Apple provides the App Store Connect API, but it does not offer a feature to fetch reviews.
Public URL API (RSS) ⚠️
Apple provides a public APP review RSS subscription URL, and offers both RSS XML and JSON formats.
1
https://itunes.apple.com/國家碼/rss/customerreviews/id=APP_ID/page=1/sortBy=mostRecent/json
Country codes: Refer to this document.
APP_ID: Go to the App’s web page, and you will get the URL: https://apps.apple.com/tw/app/APPNAME/id 12345678, the number after “id” is the App ID (numbers only).
page: Requests can be made for pages 1 to 10; beyond that, retrieval is not possible.
sortBy:
mostRecent/json
requests the latest data in JSON format. You can also change it tomostRecent/xml
for XML format.
The evaluation data is returned as follows:
rss.json:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
{
"author": {
"uri": {
"label": "https://itunes.apple.com/tw/reviews/id123456789"
},
"name": {
"label": "test"
},
"label": ""
},
"im:version": {
"label": "4.27.1"
},
"im:rating": {
"label": "5"
},
"id": {
"label": "123456789"
},
"title": {
"label": "A Wonderful Existence!"
},
"content": {
"label": "Life is worthwhile now~",
"attributes": {
"type": "text"
}
},
"link": {
"attributes": {
"rel": "related",
"href": "https://itunes.apple.com/tw/review?id=123456789&type=Purple%20Software"
}
},
"im:voteSum": {
"label": "0"
},
"im:contentType": {
"attributes": {
"term": "Application",
"label": "Application"
}
},
"im:voteCount": {
"label": "0"
}
}
Advantages:
Public access without authentication steps required
Simple and easy to use
Disadvantages:
This RSS API is very outdated and has not been updated.
The feedback information is too limited (no comment time, edited review?, replied?)
Encountering data corruption issues (older data occasionally appears suddenly on later pages)
Access up to 10 pages
The biggest issue we encountered is 3; however, it is unclear whether this is a problem with the Bot tool we used or with the RSS URL data.
Private URL API ✅
This method is somewhat unconventional and was a sudden idea I came up with; however, after reviewing other Review Bots, I found that many websites use this approach. It should be fine, and I saw tools doing this 4 to 5 years ago, though I didn’t study it in depth back then.
Advantages:
Same as Apple backend data
Complete and up-to-date data
More detailed filtering options available
Deeply integrated app tools also use this method (AppRadar/AppReviewBot…).
Disadvantages:
Unofficial Methods (Unconventional Approaches)
Because Apple enforces full two-step verification, login sessions need to be updated regularly.
Step 1 — Sniff the API that loads data for the App Store Connect backend review section:
To access Apple’s backend, use the command:
1
https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/apps/APP_ID/platforms/ios/reviews?index=0&sort=REVIEW_SORT_ORDER_MOST_RECENT
This endpoint retrieves the list of reviews:
index = page offset, displaying up to 100 items at a time.
Evaluation data returned as follows:
private.json:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"value": {
"id": 123456789,
"rating": 5,
"title": "A Wonderful Existence!",
"review": "Life is worth it~",
"created": null,
"nickname": "test",
"storeFront": "TW",
"appVersionString": "4.27.1",
"lastModified": 1618836654000,
"helpfulViews": 0,
"totalViews": 0,
"edited": false,
"developerResponse": null
},
"isEditable": true,
"isRequired": false,
"errorKeys": null
}
After testing, it was found that simply including cookie: myacinfo=<Token>
can forge the request to obtain data:
The API is ready, and the required headers are known. Next, we need to find a way to automate retrieving this cookie information from the backend.
Step 2 — The Universal Fastlane
Because Apple now enforces full Two-Step Verification, automating login authentication has become more complicated. Fortunately, Fastlane, which competes with Apple’s security, implements official App Store Connect API, iTMSTransporter, and web authentication (including two-step verification). We can directly use Fastlane commands:
1
fastlane spaceauth -u <App Store Connect account (Email)>
This command completes web login verification (including two-factor authentication) and then saves the cookie into the FASTLANE_SESSION file.
You will get a string similar to the following:
1
2
3
4
5
6
7
8
9
10
11
12
!ruby/object:HTTP::Cookie
name: myacinfo value: <token>
domain: apple.com for_domain: true path: "/"
secure: true httponly: true expires: max_age:
created_at: 2021-04-21 20:42:36.818821000 +08:00
accessed_at: 2021-04-21 22:02:45.923016000 +08:00
!ruby/object:HTTP::Cookie
name: <hash> value: <token>
domain: idmsa.apple.com for_domain: true path: "/"
secure: true httponly: true expires: max_age: 2592000
created_at: 2021-04-19 23:21:05.851853000 +08:00
accessed_at: 2021-04-21 20:42:35.735921000 +08:00
Assign myacinfo = value
to get the review list.
Step 3 — SpaceShip
I originally thought Fastlane could only help us up to this point, and after that, we had to manually connect the flow of getting cookies from Fastlane and then calling the API. However, after some exploration, I found that Fastlane’s authentication module SpaceShip
has even more powerful features!
SpaceShip
SpaceShip already provides a method to fetch the review list for us: Class: Spaceship::TunesClient::get_reviews!
1
2
app = Spaceship::Tunes::login(appstore_account, appstore_password)
reviews = app.get_reviews(app_id, platform, storefront, versionId = '')
*storefront = region
Step 4 — Assembly
Fastlane and Spaceship are both written in Ruby, so we also need to use Ruby to create this Bot tool.
We can create a reviewBot.rb
file, and to run it, simply enter the following in the Terminal:
1
ruby reviewBot.rb
Done. ( *For more Ruby environment issues, see the tips at the end)
First, since the original get_reviews API parameters do not meet our needs; what I want is review data for all regions and all versions, without filtering, and with pagination support:
extension.rb:
1
2
3
4
5
6
7
8
9
# Extension Spaceship->TunesClient
module Spaceship
class TunesClient < Spaceship::Client
def get_recent_reviews(app_id, platform, index)
r = request(:get, "ra/apps/#{app_id}/platforms/#{platform}/reviews?index=#{index}&sort=REVIEW_SORT_ORDER_MOST_RECENT")
parse_response(r, 'data')['reviews']
end
end
end
So we extend a method in TunesClient ourselves, with parameters only including app_id, platform = ios
(all lowercase), and index = pagination offset.
Next, assemble the login authentication and fetch the review list:
get_recent_reviews.rb:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
index = 0
breakWhile = true
while breakWhile
app = Spaceship::Tunes::login(APPStoreConnect account (Email), APPStoreConnect password)
reviews = app.get_recent_reviews($app_id, $platform, index)
if reviews.length() <= 0
breakWhile = false
break
end
reviews.each { \|review\|
index += 1
puts review["value"]
}
end
Use while loop to iterate through all pages and stop when no content is found.
Next, add a record of the last latest timestamp, and only notify the newest messages that have not been notified:
lastModified.rb:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
lastModified = 0
if File.exists?(".lastModified")
lastModifiedFile = File.open(".lastModified")
lastModified = lastModifiedFile.read.to_i
end
newLastModified = lastModified
isFirst = true
messages = []
index = 0
breakWhile = true
while breakWhile
app = Spaceship::Tunes::login(APPStoreConnect account (Email), APPStoreConnect password)
reviews = app.get_recent_reviews($app_id, $platform, index)
if reviews.length() <= 0
breakWhile = false
break
end
reviews.each { \|review\|
index += 1
if isFirst
isFirst = false
newLastModified = review["value"]["lastModified"]
end
if review["value"]["lastModified"] > lastModified && lastModified != 0
# Do not send notification on first use
messages.append(review["value"])
else
breakWhile = false
break
end
}
end
messages.sort! { \|a, b\| a["lastModified"] <=> b["lastModified"] }
messages.each { \|message\|
notify_slack(message)
}
File.write(".lastModified", newLastModified, mode: "w+")
Simply use a .lastModified
to record the time obtained during the last execution.
No notification on first use, otherwise it will spam all at once
Final step, compose the push notification & send it to Slack:
slack.rb:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# Slack Bot
def notify_slack(review)
rating = review["rating"].to_i
color = rating >= 4 ? "good" : (rating >= 2 ? "warning" : "danger")
like = review["helpfulViews"].to_i > 0 ? " - #{review["helpfulViews"]} :thumbsup:" : ""
date = review["edited"] == false ? "Created at: #{Time.at(review["lastModified"].to_i / 1000).to_datetime}" : "Updated at: #{Time.at(review["lastModified"].to_i / 1000).to_datetime}"
isResponse = ""
if review["developerResponse"] != nil && review["developerResponse"]['lastModified'] < review["lastModified"]
isResponse = " (Reply is outdated)"
end
edited = review["edited"] == false ? "" : ":memo: User updated review#{isResponse}:"
stars = "★" * rating + "☆" * (5 - rating)
attachments = {
:pretext => edited,
:color => color,
:fallback => "#{review["title"]} - #{stars}#{like}",
:title => "#{review["title"]} - #{stars}#{like}",
:text => review["review"],
:author_name => review["nickname"],
:footer => "iOS - v#{review["appVersionString"]} - #{review["storeFront"]} - #{date} - <https://appstoreconnect.apple.com/apps/APP_ID/appstore/activity/ios/ratingsResponses\|Go To App Store>"
}
payload = {
:attachments => [attachments],
:icon_emoji => ":storm_trooper:",
:username => "ZhgChgLi iOS Review Bot"
}.to_json
cmd = "curl -X POST --data-urlencode 'payload=#{payload}' SLACK_WEB_HOOK_URL"
system(cmd, :err => File::NULL)
puts "#{review["id"]} send Notify Success!"
end
SLACK_WEB_HOOK_URL
= Incoming WebHook URL
Final Result
appreviewbot.rb:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
require "Spaceship"
require 'json'
require 'date'
# Config
$slack_web_hook = "Target notification web hook url"
$slack_debug_web_hook = "Notification web hook url for bot errors"
$appstore_account = "APPStoreConnect account (Email)"
$appstore_password = "APPStoreConnect password"
$app_id = "APP_ID"
$platform = "ios"
# Extension Spaceship->TunesClient
module Spaceship
class TunesClient < Spaceship::Client
def get_recent_reviews(app_id, platform, index)
r = request(:get, "ra/apps/#{app_id}/platforms/#{platform}/reviews?index=#{index}&sort=REVIEW_SORT_ORDER_MOST_RECENT")
parse_response(r, 'data')['reviews']
end
end
end
# Slack Bot
def notify_slack(review)
rating = review["rating"].to_i
color = rating >= 4 ? "good" : (rating >= 2 ? "warning" : "danger")
like = review["helpfulViews"].to_i > 0 ? " - #{review["helpfulViews"]} :thumbsup:" : ""
date = review["edited"] == false ? "Created at: #{Time.at(review["lastModified"].to_i / 1000).to_datetime}" : "Updated at: #{Time.at(review["lastModified"].to_i / 1000).to_datetime}"
isResponse = ""
if review["developerResponse"] != nil && review["developerResponse"]['lastModified'] < review["lastModified"]
isResponse = " (Customer service reply is outdated)"
end
edited = review["edited"] == false ? "" : ":memo: User updated the review#{isResponse}:"
stars = "★" * rating + "☆" * (5 - rating)
attachments = {
:pretext => edited,
:color => color,
:fallback => "#{review["title"]} - #{stars}#{like}",
:title => "#{review["title"]} - #{stars}#{like}",
:text => review["review"],
:author_name => review["nickname"],
:footer => "iOS - v#{review["appVersionString"]} - #{review["storeFront"]} - #{date} - <https://appstoreconnect.apple.com/apps/APP_ID/appstore/activity/ios/ratingsResponses\|Go To App Store>"
}
payload = {
:attachments => [attachments],
:icon_emoji => ":storm_trooper:",
:username => "ZhgChgLi iOS Review Bot"
}.to_json
cmd = "curl -X POST --data-urlencode 'payload=#{payload}' #{$slack_web_hook}"
system(cmd, :err => File::NULL)
puts "#{review["id"]} send Notify Success!"
end
begin
lastModified = 0
if File.exists?(".lastModified")
lastModifiedFile = File.open(".lastModified")
lastModified = lastModifiedFile.read.to_i
end
newLastModified = lastModified
isFirst = true
messages = []
index = 0
breakWhile = true
while breakWhile
app = Spaceship::Tunes::login($appstore_account, $appstore_password)
reviews = app.get_recent_reviews($app_id, $platform, index)
if reviews.length() <= 0
breakWhile = false
break
end
reviews.each { \|review\|
index += 1
if isFirst
isFirst = false
newLastModified = review["value"]["lastModified"]
end
if review["value"]["lastModified"] > lastModified && lastModified != 0
# Do not send notifications on first run
messages.append(review["value"])
else
breakWhile = false
break
end
}
end
messages.sort! { \|a, b\| a["lastModified"] <=> b["lastModified"] }
messages.each { \|message\|
notify_slack(message)
}
File.write(".lastModified", newLastModified, mode: "w+")
rescue => error
attachments = {
:color => "danger",
:title => "AppStoreReviewBot Error occurs!",
:text => error,
:footer => "*Due to Apple technical restrictions, the precise review scraping feature requires re-login about once a month. Please understand."
}
payload = {
:attachments => [attachments],
:icon_emoji => ":storm_trooper:",
:username => "ZhgChgLi iOS Review Bot"
}.to_json
cmd = "curl -X POST --data-urlencode 'payload=#{payload}' #{$slack_debug_web_hook}"
system(cmd, :err => File::NULL)
puts error
end
Also added begin…rescue (try…catch) protection. If an error occurs, a Slack notification is sent for us to check (usually due to session expiration).
Finally, just add this script to crontab/schedule or other scheduling tools for regular execution!
Renderings:
Other Free Options
AppFollow: Using the Public URL API (RSS) is just acceptable.
feedis.io: Using the Private URL API requires providing your account credentials to them.
TradeMe/ReviewMe: Self-hosted service (node.js), we originally used this but encountered the issues mentioned above.
JonSnow: Self-hosted service (GO), supports one-click deployment to Heroku, author: @saiday
Warm Tips
1.⚠️Private URL API method requires re-verification every 30 days for accounts with two-factor authentication, and there is currently no solution; if you can create an account without two-factor authentication, you can use it smoothly without issues.
#important-note-about-session-duration
2.⚠️ Whether free, paid, or self-hosted as in this article; never use a developer account. Always create a separate App Store Connect account with only “Customer Support” access to prevent security issues.
It is recommended to use rbenv to manage Ruby, as the system’s built-in version 2.6 may cause conflicts.
On macOS Catalina, if you encounter GEM or Ruby environment errors, refer to this reply for a solution.
Problem Solved!
After the above journey, I have a better understanding of how Slack Bots work; how the iOS App Store crawls review content, and I also tried Ruby! It’s quite nice to write!
If you have any questions or feedback, feel free to contact me.
This post was originally published on Medium (View original post), and automatically converted and synced by ZMediumToMarkdown.