苹果ASA归因对接以及API接入

一、归因概要

广告归因,目的是用于衡量广告带来的激活用户的成本以及后续进一步的用户质量表现。 Apple Ads 广告平台是基于 App Store(站内广告),同时属于自归因平台(通常称为 SAN)。这两个因素,决定了 ASA 与其他大部分广告平台(站外广告)的区别。 ASA 广告投放前,无需创建投放链接、监测链接,ASA 归因由 Apple 自身完成,可以保证用户隐私同时还能做到用户级归因 ,目前获取归因数据可采用自归因或第三方归因的方式。

以下是该方案的要点和广告主应对措施:

  • 此归因方案仅适用 Apple Search Ads 广告;仅支持 iOS 14.3 及更高版本;14.3 之前的版本,需使用 iAd Framework

    • 从 2023 年 2 月 7 日开始,iAd 框架停止用于归因 Apple Ads 的广告安装。

    • 所有通过iAd Framework发送的请求都会收到 "iad-attribution"=false的错误,具体请查看iAd Changelog | Apple Developer Documentation

    • 开发者需实施 AdSercices 框架以归因来自 Apple Ads 的广告安装。此框架于 2021 年 1 月发布,如果已完成实施、或使用主流 MMP 的开发者无需做更改。

    • AdServices[3] 仅支持 iOS 14.3 及更高版本的设备。

    • 届时 14.2 及更低旧版本的设备将无法归因苹果广告安装。

  • 此方案将极大提高 ASA 广告安装的激活率

  • 安装到激活的差距逐渐降至极低

  • 此方案涉及前后端的系统开发,需自己归因的开发者应尽早制定计划、安排实施

  • 使用 MMP 服务的开发者,与您的服务提供商沟通,了解其SDK对此方案的支持进度

  • 无论开发者自己实施还是采用 MMP,均需要发布新的app版本

  • 针对 iOS 14 及更高版本的设备,LAT 的概念已失效,受众的年龄和性别定向不再排除限制跟踪的用户

  • 什么是ASA?

      ASA(Apple Search Ads),也叫ASM(App Store Marketing)---应用商店广告,即用户在App Store 搜索应用时出现在搜索结果上方的广告。

      搜索广告有三种展现形式:标题+icon+截图/描述/视频, App 在搜索广告上的展示内容与其在自然搜索排名下的内容相同,开发者不必也不能单独为搜索广告设置素材,且无法选择或设置广告具体以哪种方式展现;

  • ASA广告竞价逻辑

ASA广告类似谷歌搜索广告,属于竞价广告模式,以点击计费;

广告展现量受5个因素影响,分别是相关性、出价、关键词热度、竞争者、投放位置;广告单价受3个因素影响,分别是相关性、出价、竞争者;

1)相关性(关键词和APP之间的相关性):一个关键词和这个 App 之间的相关性是由这个 App 的元数据和用户 行为所决定的;元数据即 App 的标题、icon、截图、描述等。

2)出价:在其他参数不变的情况下,价高者得;

其他影响因素:产品权重(安装总量/总榜/分类榜单越高,是受到用户喜爱得产品)、关键词热度(aso吸引力-100关键词的覆盖)

二、归因实施说明

归因业务流程图

4cc231f0a6354af0ad35fbf815004a6c.png

这个归因方案,包含两部分:客户端的 AdServices 框架,和从苹果Search Ads归因服务器获取归因数据的 RESTful API。

下图说明了结合使用 AdServices 框架和 RESTful API 来完成归因。

2e6a719abe614bd89afc79934d49f596.png

Request Token 获取 token

  • (NSString *)attributionTokenWithError:(NSError * _Nullable *)error;

AdServices 框架返回的 token 为字符串类型,并且只有 24 小时有效期。

AAAttributionErrorCode 归因错误枚举值 

typedef enum AAAttributionErrorCode : NSInteger {// 没有返回 token,网络不可用。AAAttributionErrorCodeNetworkError = 1,// 没有返回 token,发生了内部错误。AAAttributionErrorCodeInternalError = 2,// 没有返回 token,操作系统平台不支持。AAAttributionErrorCodePlatformNotSupported = 3
} AAAttributionErrorCode;

Request attribution 获取归因数据

您可以将 token 提供给 MMP,或者使用该 token 在 24 小时内进行 POST API 调用,以获取归因数据。在请求正文中带上 token:

POST https://api-adservices.apple.com/api/v1/
yourtokenyourtoken

Response Codes 返回状态码

Response状态码Description描述
200成功。API 找到了匹配的归因记录,返回值包含 attribution=true。如果API没有找到对应的归因记录,返回值为 attribution=false。在这种情况,状态码 200 仅表示服务器有数据返回。
400token 无效。
404没有找到。API 无法获取到请求的归因记录。Tokens 只有 24 小时的有效期。如果请求超过了 24 小时,苹果会返回 404 状态码。如果 token 是有效的,一个最佳实践:最多重试 3 次,每次间隔 5 秒钟。
500服务器暂时关闭或无法访问。请求可能是有效的,但是你需要选择合适的时间点进行重试。

代码示例:

- #import <AdServices/AdServices.h>
(void) methodToGetToken {if (@available(iOS 14.3, *)) {NSError *error;NSString *token = [AAAttribution attributionTokenWithError:&error];if (token != nil) {// 发送POST请求归因数据}} else {// Fallback on earlier versions}
}
- (void) attributionWithToken:(NSString *)token {NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];NSURL *url = [NSURL URLWithString:@"https://api-adservices.apple.com/api/v1/"];NSMutableURLRequest 
request = [NSMutableURLRequest requestWithURL:urlcachePolicy:NSURLRequestUseProtocolCachePolicytimeoutInterval:60.0];[request addValue:@"application/json" forHTTPHeaderField:@"Content-Type"];[request setHTTPMethod:@"POST"];
*  NSData*postData = [token dataUsingEncoding:NSUTF8StringEncoding];[request setHTTPBody:postData];NSURLSessionDataTask *postDataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {NSError *resError;NSMutableDictionary *resDic = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&resError];}];[postDataTask resume];
}

 

字段类型说明
attributionBoolean如果用户在应用下载前 30 天点击了 App Store、Apple News 以及 Stocks,则其值为 true。如果 API 找不到匹配的归因记录,则为false。
orgIdInteger广告系列所属的账户 ID。
campaignIdInteger广告系列 ID。
conversionTypeString表明是否首次下载。"Redownload" 说明用户在本设备下载/卸载过,或者用同一账户在其他设备下载过。
clickDateDate/time string用户点击相应广告的日期和时间。此字段仅出现在详细归因数据包中。
adGroupIdInteger广告组 ID。
countryOrRegionString国家或地区。
keywordIdInteger关键字的 ID。
creativeSetIdInteger广告素材集的 ID。

 

这里只要是attribution=true,就意味着用户是通过点击广告下载的app。反之会返回false。原则是用户在下载app之前,30天内是否有广告点击的记录。所以客户如果只想判断用户是不是通过广告带来的,只看这个字段就可以,=true的情况下click date一定是在30天内的。

conversiontype是用来判断用户是首次安装还是重新安装,对于判断用户是否来自广告投放没有太大的关系。

客户端调用AdService框架

  • AdServices framework发起调用请求生成 Token 。

  • AdServices framework生成Token 返回(Token有效期24小时)。更多详请,请参阅 attributionToken

  • 使⽤Token向Attribution API请求结果

  • Attribution API返回true or false,判断该客户是否点击过苹果搜索广告

    • True: ⽤户在过去30天内曾经点击过苹果广告,记录最近一次用户点击广告

    • False: ⽤户在过去30天没有点击过苹果广告(用户为IOS14.3以下版本请求Token就会报错)

  • MMP 或开发⼈员使⽤ Token 发起 RESTful API 获取归因记录请求,苹果的归因服务器响应请求。更多详请,请参阅Attribution Payload。

  • API 返回的归因数据中的键值与Apple Ads ⼴告系列管理 API 中的⼴告系列的字段相对应。更多详情,请参阅 Attribution Payload Descriptions。

*苹果官网参考文档:https://developer.apple.com/doc

三、ManagementAPI接⼊具体流程

授权广告主账户对应广告系列组“API只读”

  • 需要广告主提供一个开发者账号AppleID(即邮箱)

  • 授权后需要甲方到邮箱查收邀请邮件,并点击邮件中链接确认授权

开发者生成公钥+私钥,准备上传到ads.apple.com后台

  • 生成公钥私钥:

  • 如果您使用的是MacOS或类似UNIX的操作系统,OpenSSL可以原生运行。如果您使用的是Windows平台,则需要下载OpenSSL。

  • openssl ecparam -genkey -name prime256v1 -noout -out private-key.pemopenssl ec -in private-key.pem -pubout -out public-key.pem

    上传公钥私钥,生成clientid、keyid、teamid

  • 登录被授权的账号,查看右上⻆设置d41535b27c864ee29f84e7141c4fbbb1.png

  • 粘贴前⾯⽣成的公钥进去,点击保存按钮;并记录⽣成的clientid、keyid、teamid

  • 使⽤新⽣成的clientid、keyid、teamid,⽣成client_secret

41ff03a0d842462eb946cf38dad24df3.png

  • 生成出来的ID示例:

clientId SEARCHADS.aeb3ef5f-0c5a-4f2a-99c8-fca83f25a9
teamId SEARCHADS.hgw3ef3p-0w7a-8a2n-77c8-scv83f25a7
keyId a273d0d3-4d9e-458c-a173-0db8619ca7d7
  • 使⽤3个id⽣成client_secret的示例

import jwt
import datetime as dt
client_id = 'SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577'
team_id = 'SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577' 
key_id = 'bacaebda-e219-41ee-a907-e2c25b24d1b2' 
audience = 'https://appleid.apple.com'
alg = 'ES256'
Define issue timestamp.
issued_at_timestamp = int(dt.datetime.utcnow().timestamp())
Define expiration timestamp. May not exceed 180 days from issue timestamp.
expiration_timestamp = issued_at_timestamp + 86400*180 
Define JWT headers.
headers = dict()
headers['alg'] = alg
headers['kid'] = key_id
Define JWT payload.
payload = dict()
payload['sub'] = client_id
payload['aud'] = audience
payload['iat'] = issued_at_timestamp
payload['exp'] = expiration_timestamp
payload['iss'] = team_id 
Path to signed private key.
KEY_FILE = 'private-key.pem' 
with open(KEY_FILE,'r') as key_file:key = ''.join(key_file.readlines())
client_secret = jwt.encode(
payload=payload,  
headers=headers,
algorithm=alg,  
key=key
)
with open('client_secret.txt', 'w') as output:output.write(client_secret.decode("utf-8"))
  • JWT请求头和请求data示例

Header
{
"alg": "ES256",
"kid": "bacaebda-e219-41ee-a907-e2c25b24d1b2"
}
Payload
{
"iss": "SEARCHADS.hgw3ef3p-0w7a-8a2n-77c8-scv83f25a7", # 您上一步获得的teamId
"iat": 2234567891, # 创建客户端密钥时的 UNIX UTC 时间戳
"exp": 2234567900, # 客户端密码过期的 UNIX UTC 时间戳。该值必须大于当前时间
"aud": "https://appleid.apple.com",
"sub": "SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577" # 您上一步获得的client_secret
}
  • 请求⽣成的client_secret示例

eyJraWQiOiJiYWNhZWJkYS1lMjE5LTQxZWUtYTkwNy1lMmMyNWIyNGQxYjIiLCJhbGciOiJFUzI1NiJ9.
eyJpc3MiOiJEcmVhbWNvbXBhbnkiLCJhdWQiOiJBdXRoZW50aWNhdG9yIiwiZXhwIjoxNTcxNjcwNjIx
LCJuYmYiOjE1NzE2NjcwMjEsInN1YiI6Im11c3RlciIsImNsaWVudF9pZCI6ImFiY2QxMjM0IiwiYWRt
aW4iOiJ0cnVlIn0.s4C3p9kVNFeRAB5tChatC3ldQX07v9mG7thL7FeEO6cClfNuiaLSgq8f8ymbfO3O
QYW_KuwaA1KYRuoy1JmKk 4DBbYLcz6aoABe0pzI5Z_6wgMzAyqz8pQtwDAcd4Idoi8JdRbtzZce9o-0
nZiFA4hVAXqYwpEYC4UU8ZmJO_z8tY4juHPTV3nDugdtqyNnmAiBoLryOfGNngQZccdY1_QvkXS1y0bg1
a0k8cVVtnq- _93fYJIt9Z64CTvlH3uOeh7uaEv3nIxpXhvhkTySpUmY8e04TO09oTyZijiloByv3KFQ9
2OOJ8L 5N5_CeEc5p9LWjT1pcX8ATamOycZz2Q

使⽤client_secret,请求苹果接⼝⽣成access_token

access_token存在过期的情况,通常为1小时,需要定时维护

  • 请求获取 access_token 示例

curl -X POST \
-H 'Host: appleid.apple.com' \
-H 'Content-Type: application/x-www-form-urlencoded' \
'https://appleid.apple.com/auth/oauth2/token?grant_type=client_credentials&
client_id=SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577&client_secret=eyJ0
eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.zI1NiIsImprdSI6Imh0dHBzOi8vYXV0aC5kZXYuYXB
pLnJpY29oL3YxL2Rpc2NvdmVyeS9rZXlzIiwia2lkIjoiMmIyZTgyMTA2NzkxZGM4ZmFkNzgxNW
Q3ZmI1NDRhNjJmNzJjMTZmYSJ9.eyJpc3MiOiJodHRwczovL2F1dGguZGV2LmFwaS5yaWNvaC8iL
CJhdWQiOiJodHRwczovL2lwcy5kZXYuYXBpLnJpY29oLyIsImlhdCI6MTQ5MDg1Mjc0MSwiZXhwI
joxNDkwODU2MzQxLCJjbGllbnRfaWQiOiI4ODQwMWU1MS05MzliLTQ3NzktYjdmNy03YzlmNGIzZj
kyYzAiLCJzY29wZSI6Imh0dHBzOi8vaXBzLmRldi5hcGkucmljb2gvdjEiLCJyaWNvaF9tc3Mi
OnsibWVkaWEiOnsicXVvdGEiOjEwLCJ0aHJvdHRsZSI6eyJ2YWx1ZSI6MCwid2luZG93IjowfX1
9fQ.jVq_c_cTzgsLipkJKBjAHzm8KDehW4rFA1Yg0EQRmqWmBDlEKtpRpDHZeF6ZSQfNH2OlrBW
FBiVDV9Th091QFEYrZETZ1IE1koAO14oj4kf8TCmhiG_CtJagvctvloW1wAdgMB1_Eubz9a8oim
cODqL7_uTmA5jKFx3ez9uoqQrEKZ51g665jSI6NlyeLtj4LrxpI9jZ4zTx1yqqjQx0doYQjBPhOB
06Z5bdiVyhJDRpE8ksRCC3kDPS2nsvDAal28sMgyeP8sPvfKvp5sa2UsH78WJmTzeZWcJfX2C2ba3
xwRMB5LaaVrQZlhj9xjum0MfDpIS1hJI6p5CHZ8w&scope=searchadsorg'
  • 请求接⼝的字段说明

Header
headers = {'Content-Type': 'application/x-www-form-urlencoded','Host': 'appleid.apple.com'
}
Payload
grant_type:固定值client_credentials;
client_id:前面获取的client_id;
client_secret:前面获取的client_secret;
scope:固定值searchadsorg
  • 响应示例

{"access_token":"eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjpudWxsfQ..lXm332TFi0u2E9YZ.bVVBvsjcavoQbBnQVeDiqEzmUIlaH9zLKY6rl36A_TD8wvgvWxpyBXMQuhs-qWG_dxQ5nfuJEIxOp8bIndfLE_4a3AiYtW0BsppO3vkWxMe0HWnzglkFbKUHU3PaJbLHpimmnLvQr44wUAeNcv1LmUPaSWT4pfaBzv3dMe3PNHJJCLVLfzNlWTmPxViIivQt3xyiQ9laBO6qIQiKs9zX7KE3holGpJ-Wvo39U6ZmGs7uK9BoNBPaFtd_q914mb9ChHAKcQaxF3Gadtu_Z5rYFg.vD0iQuRwHGYVnDy27qexCw","token_type": "Bearer","expires_in": 3600,"scope": "searchadsorg"
}
  • 响应字段说明

access_token:返回的 access_token,用于请求苹果广告相关数据接口 https://developer.apple.com/documentation/apple_search_ads/calling_the_apple_search_ads_api
token_type:固定值Bearer
expires_in:token有效期,3600秒即1小时
scope:访问权限

使⽤access_token获取⼴告账户下的⼴告orgid

官方文档:Get User ACL | Apple Developer Documentation

  • 请求示例

curl --location --request GET 'https://api.searchads.apple.com/api/v4/acls'
--header 'Authorization: Bearer eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwia2
lkIjpudWxsfQ..FGvVpX2vBYfyC91X.FKDdsLoAZwsfE8lgd5Ts5vSTY-lScKSp2myVIP9Eh7H
wc-7vsXr63aHcvNaDec9p7MwFHYnCC8Zphuf-BoqhAgyEu3hg_oHuqNEhbyS7iZexDfdgScTop
zGD5IP7Kiag71n0bbKb8o68MlJV8P-faGcsZR1-FBuISuyIrP6ZdUwXjoNygNCr4RkOLLNLjng
ShnnFUICJ3Q7dvrMQk0kUI_OFP3OcfZrsbHOIPWgDxA0W6GYI000Ua9x3TPT46kW2RQ67Pk5RS
2Ft_sqPgVh8V-A.7Rs0pIsQR09nRccM8'响应示例:{"orgName": "org name example","orgId": 40669820,"currency": "USD","timeZone": "America/Los_Angeles","paymentModel": "PAYG","roleNames": ["Admin"],"parentOrgId": "27154130","displayName": "display name example"
}

使⽤ access_token 获取⼴告账户投放数据

7.1 请求头参数说明

  • Authorization 设定值时组合⽅式为:Bearer {access_token},即Bearer在前,后⾯加 access_token

  • X-AP-Context 的值为orgId={orgId},orgId=不能漏掉

  • orgId 即广告系列组ID,上面第6部分接口中获取到的orgId

7.2 请求示例 - 获取广告系列组下的广告组数据

  • 以下仅为1个示例,实际对接时可查看苹果官⽅Management API⽂档,根据业务需要确认请求接⼝

  • 下面示例的官方文档参考: Get an Ad Group | Apple Developer Documentation

7.2.1 请求说明

URL
https://api.searchads.apple.com/api/v4/campaigns/{campaignId}/adgroups/{adgroupId}请求方式
GET请求头参数
Authorization
X-AP-ContextURL中参数
campaignId  #广告系列ID
adgroupId  #广告组ID

7.2.2 响应状态码

200 请求成功
401 无权限,请检查请求参数
403 拒绝请求,请检查请求参数
404 请求资源为找到,请检查请求参数
500 苹果服务器异常,稍后重试

7.2.3 响应示例

{"id": 542370539,"campaignId": 56543219,"name": " ad group name example","cpaGoal": {"amount": "100","currency": "USD"},"startTime": "2021-04-08T12:00:22.788","endTime": "2021-04-09T12:00:22.788","automatedKeywordsOptIn": false,"defaultBidAmount": {"amount": "100","currency": "USD"},"pricingModel": "CPC","targetingDimensions": {"age": {"included": [{"minAge": 20,"maxAge": 25},{"minAge": 25,"maxAge": 55}]},"gender": {"included": ["M","F"]},"country": null,"adminArea": null,"locality": null,"deviceClass": {"included": ["IPAD","IPHONE"]},"daypart": {"userTime": {"included": [1,3,22]}},"appDownloaders": null},"orgId": 40669820,"modificationTime": "2020-04-08T19:00:24.105","status": "ENABLED","servingStatus": "RUNNING","servingStateReasons": null,"displayStatus": "RUNNING","deleted": false
}

示例代码:

import jwt
import datetime as dt
import requests
import json
import time
client_id = 'SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577'
team_id = 'SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577'
key_id = 'bacaebda-e219-41ee-a907-e2c25b24d1b2'
audience = 'https://appleid.apple.com'
alg = 'ES256'
Define issue timestamp.
issued_at_timestamp = int(dt.datetime.utcnow().timestamp())
issued_at_timestamp = int(time.time())
Define expiration timestamp. May not exceed 180 days from issue timestamp.
expiration_timestamp = issued_at_timestamp + 600
Define JWT headers.
headers = dict()
headers['alg'] = alg
headers['kid'] = key_id
Define JWT payload.
payload = dict()
payload['sub'] = client_id
payload['aud'] = audience
payload['iat'] = issued_at_timestamp
payload['exp'] = expiration_timestamp
payload['iss'] = team_id
Path to signed private key.
KEY_FILE = 'private-key.pem'
with open(KEY_FILE,'r') as key_file:
key = ''.join(key_file.readlines())
key = '''-----BEGIN EC PRIVATE KEY-----
MHcCAQEEINEGa2CfhaseOXzsHoya/UW4kgQsij9ZW6j3+GQS2zEwoAoGCCqGSM49
AwEHoUQDQgAENEUUVYnQ+hWQRT3+YEwT3m2VGS5jlO6lanvZgLDHkWfOXhBiUfF8
Cyz/X3bzbkP8pOdJZ901qGdyeW73RV1RmA==
-----END EC PRIVATE KEY-----'''
获取 client_secret
client_secret = jwt.encode(
payload=payload,
headers=headers,
algorithm=alg,
key=key
)
client_secret = client_secret.decode('utf-8')
print('-------------------- client_secret --------------------')
print(client_secret)
获取 access_token
url = 'https://appleid.apple.com/auth/oauth2/token?grant_type=client_credentials&client_id=' + client_id + '&client_secret=' + client_secret + '&scope=searchadsorg'
token = requests.post(url=url)
token_json = json.loads(token.content.decode())
access_token = token_json['access_token']
print('-------------------- access_token --------------------')
print(access_token)
acls
url = "https://api.searchads.apple.com/api/v4/acls"
headers = {'Authorization': 'Bearer %s' % (access_token),'Content-Type': 'application/json'
}
acl = requests.get(url=url, headers=headers)
print('-------------------- acl --------------------')
print(acl.text)
org_id = 123456
campaign_id = 1234567890
headers = {'Authorization': 'Bearer %s' % (access_token),'Content-Type': 'application/json','X-AP-Context': 'orgId=%d' % (org_id),
}
获取 campaigns
url = 'https://api.searchads.apple.com/api/v4/campaigns'
campaigns = requests.get(url=url, headers=headers)
print('-------------------- campaign --------------------')
print(campaigns.text)
获取 campaign report
url = 'https://api.searchads.apple.com/api/v4/reports/campaigns'
data = {# "startTime": str(dt.date.today()),# "endTime": str(dt.date.today()),"startTime": "2022-09-01","endTime": "2022-09-01","selector": {"orderBy": [{"field": "localSpend","sortOrder": "ASCENDING"}],"conditions": [{"field": "campaignId","operator": "EQUALS","values": [campaign_id],"ignoreCase": False},{"field": "deleted","operator": "IN","values": ["false","true"]}],"pagination": {"offset": 0,"limit": 1000}},"returnRowTotals": True,"granularity": "DAILY","timeZone": "ORTZ","returnGrandTotals": True,"returnRecordsWithNoMetrics": True
}
data = json.dumps(data)
reports = requests.post(url=url, data=data, headers=headers)
print('-------------------- campaign reports --------------------')
print(reports.text)
package main
import ("fmt""io/ioutil""net/http""strings""time""github.com/dgrijalva/jwt-go/v4"
)
func main() {client_id := "SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577"team_id := "SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577"key_id := "bacaebda-e219-41ee-a907-e2c25b24d1b2"audience := "https://appleid.apple.com"alg := "ES256"issued_at_timestamp := time.Now().Unix()expiration_timestamp := issued_at_timestamp + 600claim := jwt.MapClaims{"sub": client_id,"aud": audience,"iat": issued_at_timestamp,"exp": expiration_timestamp,"iss": team_id,}private_pem := `-----BEGIN EC PRIVATE KEY-----
MHcCAQEEINEGa2CfhaseOXzsHoya/UW4kgQsij9ZW6j3+GQS2zEwoAoGCCqGSM49
AwEHoUQDQgAENEUUVYnQ+hWQRT3+YEwT3m2VGS5jlO6lanvZgLDHkWfOXhBiUfF8
Cyz/X3bzbkP8pOdJZ901qGdyeW73RV1RmA==
-----END EC PRIVATE KEY-----`private_key, _ := jwt.ParseECPrivateKeyFromPEM([]byte(private_pem))token := jwt.NewWithClaims(jwt.SigningMethodES256, claim)token.Header = map[string]interface{}{"alg": alg,"kid": key_id,}client_secret, _ := token.SignedString(private_key)fmt.Println("-------------------- client_secret --------------------")fmt.Println(client_secret)// 获取 access_tokenurl := "https://appleid.apple.com/auth/oauth2/token"res_token, err := http.Post(url, "application/x-www-form-urlencoded", strings.NewReader("grant_type=client_credentials&client_id="+client_id+"&client_secret="+client_secret+"&scope=searchadsorg"))if err != nil {fmt.Println(err.Error())}defer res_token.Body.Close()access_token, _ := ioutil.ReadAll(res_token.Body)fmt.Println("-------------------- access_token --------------------")fmt.Println(string(access_token))
}

ASAToken.java

- package com.appsa.asa;
import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import org.apache.commons.io.IOUtils;
import java.io.DataOutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.interfaces.ECKey;
import java.util.Date;
import java.util.HashMap;
/**
apple ads java demo
*/
public class ASAToken {private static String client_id = "xxx";private static String team_id = "xxx";private static String key_id = "xxx";private static String aud = "https://appleid.apple.com";private static String alg = "ES256";// 生成私钥// openssl ecparam -genkey -name prime256v1 -noout -out private-key.pem// 使用私钥生成公钥// openssl ec -in private-key.pem -pubout -out public-key.pem// 将PKCS1私钥转换为PKCS8(该格式一般Java调用)// openssl pkcs8 -topk8 -inform pem -in private-key.pem -outform pem -nocrypt -out private-key-new.pemprivate static final String PRIVATE_KEY_FILE_256 = "/Users/xxx/Downloads/private-key-new.pem";public static void main(String[] args) {System.out.println("client_id:" + client_id);System.out.println("  team_id:" + team_id);System.out.println("   key_id:" + key_id);try {String clientSecret = createClientSecret(PRIVATE_KEY_FILE_256);System.out.println("clientSecret 建议保存,有效期可设置最长 180 天");System.out.println(clientSecret);String url = "https://appleid.apple.com/auth/oauth2/token";String urlParameters = "grant_type=client_credentials&scope=searchadsorg&client_id=" + client_id + "&client_secret=" + clientSecret;byte[] postData = urlParameters.getBytes(StandardCharsets.UTF_8);int postDataLength = postData.length;URL obj = new URL(url);HttpURLConnection con = (HttpURLConnection) obj.openConnection();con.setDoOutput(true);con.setRequestMethod("POST");con.setRequestProperty("charset", "utf-8");con.setRequestProperty("Content-Length", Integer.toString(postDataLength));con.setRequestProperty("Host", "appleid.apple.com");con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");con.setUseCaches(false);try (DataOutputStream wr = new DataOutputStream(con.getOutputStream())) {wr.write(postData);}if (con.getResponseCode() == 200) {String result = IOUtils.toString(con.getInputStream(), StandardCharsets.UTF_8);JSONObject jsonObject = JSONObject.parseObject(result);System.out.println("access_token 有效期1个小时");System.out.println(jsonObject.getString("access_token"));}} catch (Exception e) {System.err.println("error");}}/*** Create a Client Secret** @return client secret*/public static String createClientSecret(String privateKeyPath) throws Exception {Algorithm algorithm = Algorithm.ECDSA256((ECKey) PemUtils.readPrivateKeyFromFile(privateKeyPath, "EC"));return JWT.create().withIssuer(team_id).withAudience(aud).withHeader(new HashMap() {{put("alg", alg);put("kid", key_id);}}).withSubject(client_id).withIssuedAt(new Date()).withExpiresAt(new Date(System.currentTimeMillis() + 86400 * 180 * 1000L)).sign(algorithm);}
}

PemUtils.java

package com.appsa.asa;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
public class PemUtils {private static byte[] parsePEMFile(File pemFile) throws IOException {if (!pemFile.isFile() || !pemFile.exists()) {throw new FileNotFoundException(String.format("The file '%s' doesn't exist.", pemFile.getAbsolutePath()));}PemReader reader = new PemReader(new FileReader(pemFile));PemObject pemObject = reader.readPemObject();byte[] content = pemObject.getContent();reader.close();return content;}private static PublicKey getPublicKey(byte[] keyBytes, String algorithm) {PublicKey publicKey = null;try {KeyFactory kf = KeyFactory.getInstance(algorithm);EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);publicKey = kf.generatePublic(keySpec);} catch (NoSuchAlgorithmException e) {System.out.println("Could not reconstruct the public key, the given algorithm could not be found.");} catch (InvalidKeySpecException e) {System.out.println("Could not reconstruct the public key");}return publicKey;}private static PrivateKey getPrivateKey(byte[] keyBytes, String algorithm) {PrivateKey privateKey = null;try {KeyFactory kf = KeyFactory.getInstance(algorithm);EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);privateKey = kf.generatePrivate(keySpec);} catch (NoSuchAlgorithmException e) {System.out.println("Could not reconstruct the private key, the given algorithm could not be found.");} catch (InvalidKeySpecException e) {System.out.println("Could not reconstruct the private key.");}return privateKey;}public static PublicKey readPublicKeyFromFile(String filepath, String algorithm) throws IOException {byte[] bytes = PemUtils.parsePEMFile(new File(filepath));return PemUtils.getPublicKey(bytes, algorithm);}public static PrivateKey readPrivateKeyFromFile(String filepath, String algorithm) throws IOException {byte[] bytes = PemUtils.parsePEMFile(new File(filepath));return PemUtils.getPrivateKey(bytes, algorithm);}
}

四、注意事项总结

1. 框架须知

  • AdServices.framework 这个框架是用于 ASA 归因的,不受 ATT 约束,就是无论用户是否允许跟踪,都可以归因,区别为是否能获取点击时间,仅支持 14.3 及更高版本系统,需 XCode 12.3 及更高版本支持,设置为 optional。

  • AppTrackingTransparency.framework 在 iOS 14 及更高版本用于征求用户跟踪许可的框架,就是弹窗询问用户是否同意跟踪;在 iOS 14.5 上苹果将强制要求开发者实施,也是获取 IDFA 的前提。

2.token相关注意事项

  • AdServices 获取的 token 不可作为唯一设备标识

  • 获取 token 需要设备联网,并且要做超时及容错处理

  • 获取token后需等待五秒再发送请求给归因API

  • 获取 token 的步骤,可以借助日志系统收集相关信息,用于排查问题和代码优化

  • APP首次激活时获取token,建议获取网络权限后,等待500ms-1000ms再做token请求

  • 如果 App 退出前台,在下一次打开进行 token 请求重试

  • 对token请求成功率进行监测,建议成功率下降>=5%应触发预警机制,并根据报错调查原因

  • token 有效期为 24 小时

  • 当 token 无效时,接口响应码为 400

  • 当 token 过期(有效期为 24 小时)时,接口响应码为 404

3.数据归因相关注意事项

  • AdServices 的 restful api 请求,失败后 每隔 5 秒重试,建议最多 3 次 ,也可根据需求增加重试次数,如重试之后仍然没有正常,统计返回的报错日志

  • 有数据差合理,如进行反馈需提供 APP Id、发生问题的时间、API返回结果

  • Apple Search Ads归因窗口期为30天,用户30天以前点击的广告我们无法归因

  • Apple Search Ads采用末次归因,用户如果30天或一天内惦记了搜索标签,又搜了关键词,归因返回的是用户最后一次点击广告的信息

  • 目前苹果搜索广告归因仅支持Adservice,并且Adservice仅支持14.3及以上的用户,14.3以下的用户不会被归因到,这些用户会被计入自然量

  • 14.3以下的用户还可请求API,返回结果为attribution = false

  • 目前14.3以下的用户占比非常低,过去4年推出的ipnone 90%使用ios16,81%所有设备使用ios16

4.归因字段缺失原因

  • AdServices 归因在 ATT弹框之前,不返回字段clickDate

  • ATT弹框后,用户不允许跟踪,不返回字段clickDate

  • AdServices 归因建议在 ATT 弹窗之后,用户如允许跟踪,则可以获得 clickDate

  • 开启 Search Match(搜索匹配)的广告组带来的激活,不返回字段 keywordId

  • 默认素材带来的激活,不返回字段 adId

5.客户端处理逻辑参考

  • 由于各种原因导致的获取归因包失败时,需要做容错处理,及时进行重试(必需)

  • 重试多次仍然失败的,应用在下次启动时再进行获取(必需)

  • 当归因包返回的 attribution 为 false,7 天后再请求归因(建议)

  • iOS 14.3 以下,设置项为限制跟踪时,7 天后再次请求归因(建议)

  • 当归因包返回的 attribution 为 true 时,30 天后再请求归因(建议)

 

五、参考文档

Apple Ads 归因 API

Apple Ads 归因 API 文档 (PDF)

ad_services

Apple Search Ads

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/474603.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

内容占位符:Kinetic Loader HTML+CSS 使用CSS制作三角形原理

内容占位符 前言 随着我们对HTML和CSS3的学习逐渐深入&#xff0c;相信大家都已经掌握了网页制作的基础知识&#xff0c;包括如何使用HTML标记构建网页结构&#xff0c;以及如何运用CSS样式美化页面。为了进一步巩固和熟练这些技能&#xff0c;今天我们一起来完成一个有趣且实…

【金融风控项目-08】:特征构造

文章目录 1.数据准备1.1 风控建模特征数据1.2 人行征信数据1.3 据之间的内在逻辑 2 样本设计和特征框架2.1 定义观察期样本2.2 数据EDA(Explore Data Analysis)2.3 梳理特征框架 3 特征构造3.1 静态信息和时间截面特征3.2 未来信息问题3.2.1 未来信息案例3.2.2 时间序列特征的未…

Linux网络:HTTPS协议

Linux网络&#xff1a;HTTPS协议 加密方式对称加密非对称加密混合加密中间人攻击 证书数据签名CA认证 HTTPSSSL/TSLHTTPS 在HTTP协议中&#xff0c;所有的数据都采用明文的形式传输&#xff0c;这就会导致数据非常容易泄露&#xff0c;只要拿到HTTP报文&#xff0c;就可以窃取各…

Redis配置主从架构、集群架构模式 redis主从架构配置 redis主从配置 redis主从架构 redis集群配置

Redis配置主从架构、集群架构模式 redis主从架构配置 redis主从配置 redis主从架构 redis集群配置 1、主从模式1.1、主节点配置1.2、从节点配置1.3、测试 2、集群模式 1、主从模式 1.1、主节点配置 # 监听所有网络接口 bind 0.0.0.0# cluster-enabled表示为集群模式&#xff…

柔性仿人手指全覆盖磁皮肤,具备接触觉和运动觉的双模态感知能力

人体精细触觉和本体运动觉依赖于相同类型的感受器&#xff0c;这些感受器位于肌肉、肌腱、关节和皮肤中&#xff0c;负责感知轻触、挠痒、细微压力、形状变化、肌肉张力、肌腱拉伸和关节位置变化等信息。因此方斌教授团队着手于具有高精度、小尺寸、可定制等优势的磁触觉传感器…

【蓝桥杯C/C++】I/O优化技巧:cin.tie(nullptr)的详解与应用

文章目录 &#x1f4af;前言&#x1f4af;I/O流的基本概念&#x1f4af;cin.tie(nullptr)使用场景底层机制与ios::sync_with_stdio(false) 的搭配使用手动刷新输出流 &#x1f4af;使用示例和性能对比示例代码 &#x1f4af;常见误区和注意事项进一步优化&#xff1a;快速输入输…

字节青训-判断数组是否单调、判断回旋镖的存在、字符串解码问题、小F的矩阵值调整、数字字符串中圆圈的数量计算 、小Q的非素数和排列问题

目录 一、判断数组是否单调 问题描述 测试样例 解题思路&#xff1a; 解题思路 数据结构选择 算法步骤 最终代码&#xff1a; 运行结果&#xff1a; ​编辑 二、判断回旋镖的存在 问题描述 测试样例 解题思路&#xff1a; 解题思路 算法步骤 最终代码&#xff…

ArkTS组件结构和状态管理

1. 认识基本的组件结构 ArkTS通过装饰器Component 和Entry 装饰 struct 关键字声明的数据结构&#xff0c;构成一个自定义组件 自定义组件中提供了一个build函数&#xff0c;开发者需要在函数内以链式调用的方式进行基本的UI描述&#xff0c;UI描述的方法请参考UI描述规范srtuc…

14. 乘法口诀挑战赛

文章目录 概要整体架构流程技术细节小结 1. 概要 ~ Jack Qiao对米粒说&#xff1a;“为了帮助孩子们更好地学习乘法&#xff0c;智慧镇的镇长决定举办一场特别的活动——“乘法口诀挑战赛”。比赛的规则非常简单&#xff1a;参与者需要输入一个正整数 n&#xff0c;然后系统会…

环形缓冲区 之 STM32 串口接收的实现

STM32串口数据接收环形缓冲区接收实例说明 ...... 矜辰所致前言 关于环形缓冲区&#xff0c;网上有大量的理论说明文章&#xff0c;在有些操作系统中&#xff0c;会有实现环形缓冲区的代码&#xff0c;比如 RT-Thread 的 ringbuffer.c 和 ringbuffer.h 文件&#xff0c;Li…

Matplotlib | 理解直方图中bins表示的数据含义

引出问题 hist作图中 bins 发生变换 y轴的数据也变化 想不通 不是说y轴计算的是频率吗 频率既然是定值 为什么y轴的数据就还会变化&#xff1f;那我确定了bins的数值 我想获得bins内各个柱子&#xff08;bin&#xff09;中数据点的数量如何获得 bins的变化 先看一组数据 da…

【Linux】基础02

Linux编译和调试 VI编辑文件 vi : 进入文件编辑 是命令行模式 i &#xff1a;从光标处进入插入模式 dd : 删除光标所在行 n dd 删除指定行数 Esc &#xff1a; 退出插入模式 &#xff1a; 冒号进入末行模式 :wq : 保存退出 :q &#xff1a; 未修改文件可以退出 :q! …

Linux网络——套接字编程

1. 网络通信基本脉络 基本脉络图如上&#xff0c;其中数据在不同层的叫法不一样&#xff0c;比如在传输层时称为数据段&#xff0c;而在网络层时称为数据报。我们可以在 Linux 中使用 ifconfig 查看网络的配置&#xff0c;如图 其中&#xff0c;inet 表示的是 IPv4&#xff0c;…

深度学习的实践层面

深度学习的实践层面 设计机器学习应用 在训练神经网络时&#xff0c;超参数选择是一个高度迭代的过程。我们通常从一个初步的模型框架开始&#xff0c;进行编码、运行和测试&#xff0c;通过不断调整优化模型。 数据集一般划分为三部分&#xff1a;训练集、验证集和测试集。常…

Jmeter 如何导入证书并调用https请求

Jmeter 如何导入证书并调用https请求 通过SSL管理器添加证书文件 支持添加的文件为.p12&#xff0c;.pfx&#xff0c;.jks 如何将pem文件转换为pfx文件&#xff1f; 在公司内部通常会提供3个pem文件。 ca.pem&#xff1a;可以理解为是根证书&#xff0c;用于验证颁发的证…

LabVIEW 温湿度测试与监控系统

煤炭自燃是煤矿和煤炭储存领域面临的重大安全隐患&#xff0c;尤其是在煤炭堆积和运输过程中&#xff0c;温湿度变化会直接影响煤体的氧化速率和自燃倾向。传统的监测手段通常存在实时性差、数据处理复杂等问题&#xff0c;难以准确评估煤自燃的风险。因此&#xff0c;设计了一…

IDEA 开发工具常用快捷键有哪些?

‌在IDEA中&#xff0c;输出System.out.println()的快捷键是sout&#xff0c;输入后按回车&#xff08;或Tab键&#xff09;即可自动补全为System.out.println()‌‌。 此外&#xff0c;IDEA中还有一些其他常用的快捷键&#xff1a; 创建main方法的快捷键是psvm&#xff0c;代…

KF UKF

我需要Kalman 现在&#xff0c;主要是用来处理检测问题情况里的漏检&#xff0c;因为模拟了一段2D&#xff0c; &#xff08;x&#xff0c;y&#xff09;的数据&#xff0c;为了看效果&#xff0c;画的线尽量简单一点&#xff1a; import numpy as np import matplotlib.pyplo…

多品牌摄像机视频平台EasyCVR视频融合平台+应急布控球:打造城市安全监控新体系

在当今快速发展的智慧城市和数字化转型浪潮中&#xff0c;视频监控技术已成为提升公共安全、优化城市管理、增强应急响应能力的重要工具。EasyCVR视频监控平台以其强大的多协议接入能力和多样化的视频流格式分发功能&#xff0c;为用户提供了一个全面、灵活、高效的视频监控解决…

第8章硬件维护-8.2 可维护性和可靠性验收

8.2 可维护性和可靠性验收 可维护性和可靠性验收非常重要&#xff0c;硬件维护工程师在后端发现问题后&#xff0c;总结成可维护性和可靠性需求&#xff0c;在产品立项的时候与新特性一起进行需求分析&#xff0c;然后经过设计、开发和测试环节&#xff0c;在产品中落地。这些需…