├── LICENSE ├── README.md ├── cloud ├── README.md ├── ads_links │ ├── ads_links.py │ ├── ads_links_test.py │ └── requirements.txt ├── dv360_links │ ├── dv360_links.py │ ├── dv360_links_test.py │ └── requirements.txt ├── linker │ ├── main.py │ └── requirements.txt └── message_publisher │ ├── message_publisher.py │ └── requirements.txt ├── docs ├── code-of-conduct.md └── contributing.md ├── new_analytics ├── adSenseLinks.js ├── analyticsAdminApi.js ├── audienceLists.js ├── channelGroups.js ├── easyPropertyCreation.js ├── eventCreateRules.js ├── eventEditRules.js ├── health_report.js ├── listGA4AccessBindings.js ├── listGA4AccountSummaries.js ├── listGA4Audiences.js ├── listGA4BigQueryLinks.js ├── listGA4CalculatedMetrics.js ├── listGA4ConversionEvents.js ├── listGA4CustomDimensions.js ├── listGA4CustomMetrics.js ├── listGA4DV360Links.js ├── listGA4ExpandedDatasets.js ├── listGA4FirebaseLinks.js ├── listGA4GoogleAdsLinks.js ├── listGA4KeyEvents.js ├── listGA4Properties.js ├── listGA4SA360Links.js ├── listGA4Streams.js ├── listGA4UserLinks.js ├── listRollupPropertySourceLinks.js ├── listSubpropertyEventFilters.js ├── measurementProtocolSecrets.js ├── modifyGA4Entities.js └── userAccessReport.js ├── shared ├── constants.js ├── interfaces.js ├── menu.js ├── shared.js └── sheetsMeta.js └── utils └── checks.js /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /cloud/README.md: -------------------------------------------------------------------------------- 1 | ## Google Analytics Utilities for Cloud 2 | 3 | Google Analytics Utilities for Cloud uses Cloud Functions to accomplish specific tasks. Please follow the steps below to implement the utilities you want. 4 | 5 | All of these utilities require that the [Analytics Admin API](https://console.cloud.google.com/apis/library/analyticsadmin.googleapis.com) be enabled in your Cloud project. 6 | 7 | ### Linker 8 | 9 | The linker utility addresses the following use cases: 10 | - Linking Google Ads 11 | - Linking DV360 12 | - Sending DV360 link proposals 13 | 14 | The linker utility relies on a single Google Cloud Function to loop through a list of link settings. These settings must be uploaded to a Google Cloud Storage bucket as a CSV file of your choosing. Upon completion, the function will write a results CSV file to an output storage bucket of your choosing. 15 | 16 | If it takes longer than one hour for the function to complete, it will upload a CSV file with the remaining settings to the input bucket automatically to kick off another function until all of the requests have been attempted. 17 | 18 | The following steps describe how to set up the linker utility. 19 | 20 | #### Service Account and Credentials 21 | 22 | 1. Create a service account in Google Cloud by navigating to [IAM & Admin > Service Accounts](https://console.cloud.google.com/iam-admin/serviceaccounts) 23 | 2. Give the service account a name and grant it the following access: 24 | - Storage Admin 25 | - Eventarc Event Receiver 26 | - Cloud Run Invoker 27 | 3. Save the service account. 28 | 4. Navigate to [APIs & Services > OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent). 29 | 5. Create an internal OAuth consent screen. 30 | - Enter an app name. For the purposes of this guide, we will use GA Utilities. 31 | - Enter a support email. 32 | - Enter developer contact information. 33 | - Add the following scope: 34 | - https://www.googleapis.com/auth/analytics.edit 35 | - Save the consent screen settings. 36 | 6. Navigate to [APIs & Services > Credentials](https://console.cloud.google.com/apis/credentials). 37 | 7. Create an OAuth Client ID credential. 38 | 8. Select a Web Application type. 39 | 9. Give it a recognizable name. 40 | 10. Add https://developers.google.com as an authorized JavaScript origin. 41 | 11. Add https://developers.google.com/oauthplayground as an authorized redirect URI. 42 | 12. Make note of your client ID and client secret. 43 | 13. Navigate to https://developers.google.com/oauthplayground. NOTE: Please make sure that the user with the correct access to Google Analytics, Google Ads, and DV360 completes the following steps. 44 | 14. From the list of APIs, select Google Analytics Admin API v1Beta and select https://www.googleapis.com/auth/analytics.edit. 45 | 15. In the upper righthand corner, click on the gear icon, and enter the following settings: 46 | - OAuth flow: server-side 47 | - Oath endpoints: Google 48 | - Authorization endpoint: https://accounts.google.com/o/oauth2/v2/auth 49 | - Token endpoint: https://oauth2.googleapis.com/token 50 | - Access token location: Authorization header w/ Bearer prefix 51 | - Access type: Offline 52 | - Check "Use your own OAuth Credentials" 53 | - Enter your OAuth client ID and client secret 54 | 16. After entering the OAuth 2.0 configuration information, click "Authorize APIs". 55 | 17. Click through the consent screen and then click "Exchange authorization code for tockens". 56 | 18. Make note of your refresh token and access token. 57 | 19. At the end of this process, you should have the following that will be used in future steps: 58 | - A service account 59 | - A client ID 60 | - A client secret 61 | - An access token 62 | - A refresh token 63 | 64 | #### Storage Buckets 65 | 1. Create two storage buckets. One for input files and one for output files. 66 | 67 | #### Cloud Function 68 | Please be sure to enable whatever services are required as you create the cloud function. 69 | 70 | 1. Navigate to [Cloud Functions](https://console.cloud.google.com/functions/list) and create a new function. 71 | 2. Set the environment to 2nd gen. 72 | 3. Enter a name for your function. 73 | 4. Set the region to whatever you would like or use the default. 74 | 5. Add a "Cloud Storage" trigger and set the bucket to the input bucket you created earlier. 75 | 6. Change the service account to the service account you previously created. 76 | 7. Expand the Runtime, build, connections and security settings area and enter the following settings: 77 | - Set the memory allocated to whatever you would like, though I suggest 1 GiB. 78 | - Set the timeout to 3600s (1 hour) 79 | - Set the runtime service account to the service account you created earlier. 80 | - Add the following runtime variables: 81 | - Name: OUTPUT_BUCKET, value: The name of the output bucket you created earlier 82 | - Name: CLIENT_ID, value: The client ID 83 | - Name: CLIENT_SECRET, value: The client secret 84 | - Name: ACCESS_TOKEN, value: The access token 85 | - Name: REFRESH_TOKEN, value: The refresh token 86 | - Name: TOKEN_URI, value: https://oauth2.googleapis.com/token 87 | 8. Click next and set the runtime to 3.11. 88 | 9. Set the entry point to "main" and copy the linker cloud function code into the editor. 89 | 10. Copy the setting for requirements.txt into the editor. 90 | 11. Deploy the function. 91 | 92 | #### Linker Input CSV 93 | The input CSV file must follow [this format](https://docs.google.com/spreadsheets/d/1b_uPFH2-rXavT5BD_V8nETLgedJ_dhYOjovFtpW2s2s/copy). 94 | 95 | The request type column accepts the following values: 96 | - ads 97 | - dv360 98 | - dv360\_link\_proposal 99 | 100 | The ads\_customer\_id values should not contain dashes. 101 | 102 | The ads\_personalization\_enabled, dv360\_campaign\_data\_sharing\_enabled, and dv360\_cost\_data\_sharing\_enabled columns should only be set to true or false. 103 | 104 | -------------------------------------------------------------------------------- /cloud/ads_links/ads_links.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Performs various methods for Google Analytics Ads links. 16 | 17 | Creates, updates, deletes, or lists Google Ads links for Google Analytics based 18 | on the action and settings values in the pub/sub message. 19 | 20 | """ 21 | 22 | import base64 23 | import enum 24 | import json 25 | from typing import List, Dict 26 | from cloudevents.http import CloudEvent 27 | import functions_framework 28 | from google.analytics.admin import AnalyticsAdminServiceClient 29 | from google.analytics.admin_v1alpha.types import GoogleAdsLink 30 | import google.oauth2.credentials 31 | 32 | 33 | class Action(enum.Enum): 34 | CREATE = 'create' 35 | DELETE = 'delete' 36 | UPDATE = 'update' 37 | LIST = 'list' 38 | 39 | 40 | class RequiredKeys(enum.Enum): 41 | REFRESH_TOKEN = 'refresh_token' 42 | TOKEN_URI = 'token_uri' 43 | CLIENT_ID = 'client_id' 44 | CLIENT_SECRET = 'client_secret' 45 | SCOPES = 'scopes' 46 | CUSTOMER_ID = 'customer_id' 47 | ADS_PERSONALIZATION_ENABLED = 'ads_personalization_enabled' 48 | PROPERTY_ID = 'property_id' 49 | NAME = 'name' 50 | ENABLE_LOGGING = 'enable_logging' 51 | ACTION = 'action' 52 | 53 | 54 | @functions_framework.cloud_event 55 | def main(cloud_event: CloudEvent) -> None: 56 | """Cloud Function that creates, updates, deletes, or lists Google Ads links. 57 | 58 | Args: 59 | cloud_event: The cloud event data containing values that will be used 60 | to perform various Google Ads links methods. 61 | 62 | Returns: 63 | The response or error object. 64 | """ 65 | data = cloud_event 66 | try: 67 | data = json.loads(base64.b64decode( 68 | cloud_event.data['message']['data']).decode()) 69 | except KeyError as e: 70 | print(e) 71 | return 'Invalid message. Missing "data" key.' 72 | if find_missing_keys(data): 73 | missing_keys = ','.join(find_missing_keys(data)) 74 | error_message = f'Missing keys: {missing_keys}' 75 | print(error_message) 76 | return error_message 77 | ga_client = get_ga_client( 78 | data['refresh_token'], 79 | data['token_uri'], 80 | data['client_id'], 81 | data['client_secret'], 82 | data['scopes']) 83 | response = {} 84 | action = data['action'].lower() 85 | if action == Action.CREATE.value: 86 | ads_link = GoogleAdsLink( 87 | customer_id=str(data['customer_id']), 88 | ads_personalization_enabled=data['ads_personalization_enabled']) 89 | parent = f"properties/{data['property_id']}" 90 | response = ga_client.create_google_ads_link( 91 | parent=parent, 92 | google_ads_link=ads_link) 93 | elif action == Action.UPDATE.value: 94 | ads_link = GoogleAdsLink( 95 | name=data['name'], 96 | ads_personalization_enabled=data['ads_personalization_enabled']) 97 | response = ga_client.update_google_ads_link( 98 | google_ads_link=ads_link, 99 | update_mask='*') 100 | elif action == Action.DELETE.value: 101 | response = ga_client.delete_google_ads_link(name=data['name']) 102 | elif action == Action.LIST.value: 103 | parent = f"properties/{data['property_id']}" 104 | response = ga_client.list_google_ads_links(parent=parent) 105 | if data['enable_logging']: 106 | print(response or f"{data['name']} deleted") 107 | return response 108 | 109 | 110 | def find_missing_keys(data: Dict[str, str]) -> List[str]: 111 | """Checks if required keys are missing. 112 | 113 | Args: 114 | data: The dictionary passed into the function that should have specific 115 | keys. 116 | 117 | Returns: 118 | A list that is either empty or contains the missing keys. 119 | """ 120 | missing_keys = [] 121 | for required_key in RequiredKeys: 122 | if required_key.value not in data: 123 | missing_keys.append(required_key.value) 124 | return missing_keys 125 | 126 | 127 | def get_ga_client( 128 | refresh_token: str, 129 | token_uri: str, 130 | client_id: str, 131 | client_secret: str, 132 | scopes: List[str]) -> type[AnalyticsAdminServiceClient]: 133 | """Creates the Google Analytics Admin API client. 134 | 135 | Args: 136 | refresh_token: The OAuth 2.0 refresh token. 137 | token_uri: The OAuth 2.0 authorization server’s token endpoint URI. 138 | client_id: The OAuth 2.0 client ID. 139 | client_secret: The OAuth 2.0 client secret. 140 | scopes: The OAuth 2.0 permission scopes. 141 | 142 | Returns: 143 | The Google Analytics Admin API client. 144 | """ 145 | credentials = google.oauth2.credentials.Credentials( 146 | token=None, 147 | refresh_token=refresh_token, 148 | token_uri=token_uri, 149 | client_id=client_id, 150 | client_secret=client_secret, 151 | scopes=scopes) 152 | return AnalyticsAdminServiceClient(credentials=credentials) 153 | -------------------------------------------------------------------------------- /cloud/ads_links/ads_links_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Tests for the Google Ads Links cloud function. 16 | 17 | Tests various functions for the Google Ads Links cloud function. 18 | """ 19 | 20 | import base64 21 | import json 22 | import unittest 23 | from absl.testing import absltest 24 | from absl.testing import parameterized 25 | import ads_links 26 | from cloudevents.http import CloudEvent 27 | 28 | 29 | class GoogleAdsLinksTest(parameterized.TestCase): 30 | 31 | def setUp(self): 32 | super(GoogleAdsLinksTest, self).setUp() 33 | self.attributes = { 34 | 'source': 'test', 35 | 'type': 'test', 36 | } 37 | self.data = { 38 | 'client_id': '123', 39 | 'client_secret': 'ABC', 40 | 'refresh_token': '123ABC', 41 | 'token_uri': 'google.com', 42 | 'scopes': ['https://www.googleapis.com/auth/analytics.edit'], 43 | 'enable_logging': True, 44 | 'customer_id': '123', 45 | 'action': 'create', 46 | 'ads_personalization_enabled': True, 47 | 'property_id': '1234567', 48 | 'name': 'properties/1234567/adsLinks/abcdefg', 49 | } 50 | 51 | def test_main_missing_data_raises_error(self): 52 | missing_data = CloudEvent(self.attributes, {'message': {}}) 53 | error_message = 'Invalid message. Missing "data" key.' 54 | self.assertEqual(error_message, ads_links.main(missing_data)) 55 | 56 | def test_main_missing_required_key_raises_errors(self): 57 | del self.data['client_id'] 58 | del self.data['action'] 59 | encoded_data = base64.b64encode(json.dumps(self.data).encode('utf-8')) 60 | missing_keys = CloudEvent( 61 | self.attributes, {'message': {'data': encoded_data}}) 62 | error_message = 'Missing keys: client_id,action' 63 | self.assertEqual(error_message, ads_links.main(missing_keys)) 64 | 65 | @unittest.mock.patch('ads_links.AnalyticsAdminServiceClient') 66 | def test_get_ga_client_creates_admin_client(self, mock_analytics_client): 67 | ads_links.get_ga_client( 68 | self.data['refresh_token'], 69 | self.data['token_uri'], 70 | self.data['client_id'], 71 | self.data['client_secret'], 72 | self.data['scopes']) 73 | mock_analytics_client.assert_called() 74 | 75 | @parameterized.named_parameters( 76 | dict(testcase_name='test_main_calls_create_google_ads_link', 77 | action='create', 78 | result='created'), 79 | dict(testcase_name='test_main_calls_update_google_ads_link', 80 | action='update', 81 | result='updated'), 82 | dict(testcase_name='test_main_calls_delete_google_ads_link', 83 | action='delete', 84 | result='deleted'), 85 | dict(testcase_name='test_main_calls_list_google_ads_links', 86 | action='list', 87 | result='listed')) 88 | @unittest.mock.patch('ads_links.get_ga_client') 89 | def test_main_calling_google_ads_link_methods( 90 | self, mock_ga_client, action, result): 91 | self.data['action'] = action 92 | encoded_data = base64.b64encode(json.dumps(self.data).encode('utf-8')) 93 | cloud_event = CloudEvent( 94 | self.attributes, {'message': {'data': encoded_data}}) 95 | if action == 'create': 96 | mock_ga_client().create_google_ads_link.return_value = result 97 | elif action == 'update': 98 | mock_ga_client().update_google_ads_link.return_value = result 99 | elif action == 'delete': 100 | mock_ga_client().delete_google_ads_link.return_value = result 101 | elif action == 'list': 102 | mock_ga_client().list_google_ads_links.return_value = result 103 | self.assertEqual(result, ads_links.main(cloud_event)) 104 | 105 | 106 | if __name__ == '__main__': 107 | absltest.main() 108 | -------------------------------------------------------------------------------- /cloud/ads_links/requirements.txt: -------------------------------------------------------------------------------- 1 | functions-framework==3.* 2 | google-auth 3 | google-analytics-admin -------------------------------------------------------------------------------- /cloud/dv360_links/dv360_links.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Performs various methods for Google Analytics DV360 links. 16 | 17 | Creates, updates, deletes, or lists DV360 links for Google Analytics based on 18 | the action and settings values in the pub/sub message. 19 | """ 20 | 21 | import base64 22 | from collections.abc import MutableMapping, Sequence 23 | import enum 24 | import json 25 | import logging 26 | 27 | from cloudevents.http import CloudEvent 28 | import functions_framework 29 | from google.analytics.admin import AnalyticsAdminServiceClient 30 | from google.analytics.admin_v1alpha.types import DisplayVideo360AdvertiserLink 31 | import google.cloud.logging 32 | import google.oauth2.credentials 33 | 34 | 35 | class Action(enum.Enum): 36 | """The action to perform for the DV360 link.""" 37 | CREATE = 'create' 38 | DELETE = 'delete' 39 | UPDATE = 'update' 40 | LIST = 'list' 41 | 42 | 43 | class RequiredKeys(enum.Enum): 44 | """Each of these keys are required for the cloud function to run. 45 | """ 46 | REFRESH_TOKEN = 'refresh_token' 47 | TOKEN_URI = 'token_uri' 48 | CLIENT_ID = 'client_id' 49 | CLIENT_SECRET = 'client_secret' 50 | SCOPES = 'scopes' 51 | ADVERTISER_ID = 'advertiser_id' 52 | ADS_PERSONALIZATION_ENABLED = 'ads_personalization_enabled' 53 | CAMPAIGN_DATA_SHARING_ENABLED = 'campaign_data_sharing_enabled' 54 | COST_DATA_SHARING_ENABLED = 'cost_data_sharing_enabled' 55 | PROPERTY_ID = 'property_id' 56 | NAME = 'name' 57 | ACTION = 'action' 58 | 59 | 60 | @functions_framework.cloud_event 61 | def main(cloud_event: CloudEvent) -> None: 62 | """Cloud Function that creates, updates, deletes, or lists Google Ads links. 63 | 64 | Args: 65 | cloud_event: The cloud event data containing values that will be used to 66 | perform various Google Ads links methods. 67 | 68 | Returns: 69 | The response or error object. 70 | """ 71 | log_client = google.cloud.logging.Client() 72 | log_client.setup_logging() 73 | try: 74 | data = json.loads( 75 | base64.b64decode(cloud_event.data['message']['data']).decode() 76 | ) 77 | except KeyError: 78 | logging.error('Invalid message. Missing "data" key.') 79 | return 'Invalid message. Missing "data" key.' 80 | if _find_missing_keys(data): 81 | missing_keys = ','.join(_find_missing_keys(data)) 82 | error_message = f'Missing keys: {missing_keys}' 83 | return error_message 84 | ga_client = _get_ga_client( 85 | data['refresh_token'], 86 | data['token_uri'], 87 | data['client_id'], 88 | data['client_secret'], 89 | data['scopes'], 90 | ) 91 | response = {} 92 | action = data['action'].lower() 93 | if action == Action.CREATE.value: 94 | dv360_link = DisplayVideo360AdvertiserLink( 95 | advertiser_id=str(data['advertiser_id']), 96 | ads_personalization_enabled=data['ads_personalization_enabled'], 97 | campaign_data_sharing_enabled=data['campaign_data_sharing_enabled'], 98 | cost_data_sharing_enabled=data['cost_data_sharing_enabled'], 99 | ) 100 | parent = f"properties/{data['property_id']}" 101 | response = ga_client.create_display_video_360_advertiser_link( 102 | parent=parent, display_video_360_advertiser_link=dv360_link 103 | ) 104 | elif action == Action.UPDATE.value: 105 | dv360_link = DisplayVideo360AdvertiserLink( 106 | name=data['name'], 107 | ads_personalization_enabled=data['ads_personalization_enabled'], 108 | ) 109 | response = ga_client.update_display_video_360_advertiser_link( 110 | display_video_360_advertiser_link=dv360_link, update_mask='*' 111 | ) 112 | elif action == Action.DELETE.value: 113 | response = ga_client.delete_display_video_360_advertiser_link( 114 | name=data['name'] 115 | ) 116 | elif action == Action.LIST.value: 117 | parent = f"properties/{data['property_id']}" 118 | response = ga_client.list_display_video_360_advertiser_links(parent=parent) 119 | logging.info(response or f"{data['name']} deleted") 120 | return response 121 | 122 | 123 | def _find_missing_keys(data: MutableMapping[str, str]) -> Sequence[str]: 124 | """Checks if required keys are missing. 125 | 126 | Args: 127 | data: The dictionary passed into the function that should have specific 128 | keys. 129 | 130 | Returns: 131 | A list that is either empty or contains the missing keys. 132 | """ 133 | missing_keys = [] 134 | for required_key in RequiredKeys: 135 | if required_key.value not in data: 136 | missing_keys.append(required_key.value) 137 | return missing_keys 138 | 139 | 140 | def _get_ga_client( 141 | refresh_token: str, 142 | token_uri: str, 143 | client_id: str, 144 | client_secret: str, 145 | scopes: Sequence[str], 146 | ) -> AnalyticsAdminServiceClient: 147 | """Creates the Google Analytics Admin API client. 148 | 149 | Args: 150 | refresh_token: The OAuth 2.0 refresh token. 151 | token_uri: The OAuth 2.0 authorization server’s token endpoint URI. 152 | client_id: The OAuth 2.0 client ID. 153 | client_secret: The OAuth 2.0 client secret. 154 | scopes: The OAuth 2.0 permission scopes. 155 | 156 | Returns: 157 | The Google Analytics Admin API client. 158 | """ 159 | credentials = google.oauth2.credentials.Credentials( 160 | token=None, 161 | refresh_token=refresh_token, 162 | token_uri=token_uri, 163 | client_id=client_id, 164 | client_secret=client_secret, 165 | scopes=scopes, 166 | ) 167 | return AnalyticsAdminServiceClient(credentials=credentials) 168 | -------------------------------------------------------------------------------- /cloud/dv360_links/dv360_links_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Tests for the Google Ads Links cloud function. 16 | 17 | Tests various functions for the Google Ads Links cloud function. 18 | """ 19 | 20 | import base64 21 | import json 22 | import unittest 23 | 24 | from absl.testing import absltest 25 | from cloudevents.http import CloudEvent 26 | import dv360_links 27 | 28 | 29 | class DV360LinksTest(absltest.TestCase): 30 | 31 | def setUp(self): 32 | super(DV360LinksTest, self).setUp() 33 | self.attributes = { 34 | 'source': 'test', 35 | 'type': 'test', 36 | } 37 | self.data = { 38 | 'client_id': '123', 39 | 'client_secret': 'ABC', 40 | 'refresh_token': '123ABC', 41 | 'token_uri': 'google.com', 42 | 'scopes': ['https://www.googleapis.com/auth/analytics.edit'], 43 | 'enable_logging': True, 44 | 'advertiser_id': '123', 45 | 'action': 'create', 46 | 'campaign_data_sharing_enabled': True, 47 | 'cost_data_sharing_enabled': True, 48 | 'ads_personalization_enabled': True, 49 | 'property_id': '1234567', 50 | 'name': 'properties/1234567/displayVideo360Link/abcdefg', 51 | } 52 | 53 | self.enter_context( 54 | unittest.mock.patch.object( 55 | dv360_links.google.cloud.logging, 'Client', autospec=True 56 | ) 57 | ) 58 | 59 | def test_main_missing_data_raises_error(self): 60 | missing_data = CloudEvent(self.attributes, {'message': {}}) 61 | error_message = 'Invalid message. Missing "data" key.' 62 | self.assertEqual(error_message, dv360_links.main(missing_data)) 63 | 64 | def test_main_missing_required_key_raises_errors(self): 65 | del self.data['client_id'] 66 | del self.data['action'] 67 | encoded_data = base64.b64encode(json.dumps(self.data).encode('utf-8')) 68 | missing_keys = CloudEvent( 69 | self.attributes, {'message': {'data': encoded_data}} 70 | ) 71 | error_message = 'Missing keys: client_id,action' 72 | self.assertEqual(error_message, dv360_links.main(missing_keys)) 73 | 74 | @unittest.mock.patch('dv360_links.AnalyticsAdminServiceClient') 75 | def test_get_ga_client_creates_admin_client(self, mock_analytics_client): 76 | dv360_links.get_ga_client( 77 | self.data['refresh_token'], 78 | self.data['token_uri'], 79 | self.data['client_id'], 80 | self.data['client_secret'], 81 | self.data['scopes'], 82 | ) 83 | mock_analytics_client.assert_called() 84 | 85 | @unittest.mock.patch('dv360_links.get_ga_client') 86 | def test_main_calls_create_dv360_link(self, mock_ga_client): 87 | encoded_data = base64.b64encode(json.dumps(self.data).encode('utf-8')) 88 | cloud_event = CloudEvent( 89 | self.attributes, {'message': {'data': encoded_data}} 90 | ) 91 | result = 'created' 92 | mock_ga_client().create_display_video_360_advertiser_link.return_value = ( 93 | result 94 | ) 95 | self.assertEqual(result, dv360_links.main(cloud_event)) 96 | 97 | @unittest.mock.patch('dv360_links.get_ga_client') 98 | def test_main_calls_update_dv360_link(self, mock_ga_client): 99 | self.data['action'] = 'update' 100 | encoded_data = base64.b64encode(json.dumps(self.data).encode('utf-8')) 101 | cloud_event = CloudEvent( 102 | self.attributes, {'message': {'data': encoded_data}} 103 | ) 104 | result = 'updated' 105 | mock_ga_client().update_display_video_360_advertiser_link.return_value = ( 106 | result 107 | ) 108 | self.assertEqual(result, dv360_links.main(cloud_event)) 109 | 110 | @unittest.mock.patch('dv360_links.get_ga_client') 111 | def test_main_calls_delete_dv360_link(self, mock_ga_client): 112 | self.data['action'] = 'delete' 113 | encoded_data = base64.b64encode(json.dumps(self.data).encode('utf-8')) 114 | cloud_event = CloudEvent( 115 | self.attributes, {'message': {'data': encoded_data}} 116 | ) 117 | result = 'deleted' 118 | mock_ga_client().delete_display_video_360_advertiser_link.return_value = ( 119 | result 120 | ) 121 | self.assertEqual(result, dv360_links.main(cloud_event)) 122 | 123 | @unittest.mock.patch('dv360_links.get_ga_client') 124 | def test_main_calls_list_dv360_links(self, mock_ga_client): 125 | self.data['action'] = 'list' 126 | encoded_data = base64.b64encode(json.dumps(self.data).encode('utf-8')) 127 | cloud_event = CloudEvent( 128 | self.attributes, {'message': {'data': encoded_data}} 129 | ) 130 | result = 'listed' 131 | mock_ga_client().list_display_video_360_advertiser_links.return_value = ( 132 | result 133 | ) 134 | self.assertEqual(result, dv360_links.main(cloud_event)) 135 | 136 | 137 | if __name__ == '__main__': 138 | absltest.main() 139 | -------------------------------------------------------------------------------- /cloud/dv360_links/requirements.txt: -------------------------------------------------------------------------------- 1 | functions-framework==3.* 2 | google-auth 3 | google-analytics-admin 4 | google-cloud-logging -------------------------------------------------------------------------------- /cloud/linker/main.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import functions_framework 16 | import os 17 | import io 18 | import time 19 | import pandas 20 | import google.oauth2.credentials 21 | from google.analytics.admin import AnalyticsAdminServiceClient 22 | from google.analytics.admin_v1alpha.types import GoogleAdsLink 23 | from google.analytics.admin_v1alpha.types import DisplayVideo360AdvertiserLink 24 | from google.analytics.admin_v1alpha.types import DisplayVideo360AdvertiserLinkProposal 25 | from google.cloud import storage 26 | 27 | storage_client = storage.Client() 28 | 29 | OUTPUT_BUCKET = os.environ.get('OUTPUT_BUCKET') 30 | CLIENT_ID = os.environ.get('CLIENT_ID') 31 | CLIENT_SECRET = os.environ.get('CLIENT_SECRET') 32 | ACCESS_TOKEN = os.environ.get('ACCESS_TOKEN') 33 | REFRESH_TOKEN = os.environ.get('REFRESH_TOKEN') 34 | TOKEN_URI = os.environ.get('TOKEN_URI') 35 | REQUEST_DELAY = .2 36 | 37 | 38 | @functions_framework.cloud_event 39 | def main(cloud_event): 40 | start_time = time.time() 41 | data = cloud_event.data 42 | ga_client = get_ga_client() 43 | bucket = data['bucket'] 44 | name = data['name'] 45 | file_name = 'gs://' + bucket + '/' + name 46 | df = pandas.read_csv(file_name, on_bad_lines='skip') 47 | records = df.to_dict('records') 48 | print(records) 49 | responses = [] 50 | for index, row in enumerate(records): 51 | if time.time() - start_time < 348000000: 52 | if row['request_type'] == 'ads': 53 | try: 54 | customer_id = str(row['ads_customer_id']) 55 | parent = f"properties/{row['ga4_property_id']}" 56 | response = ga_client.create_google_ads_link( 57 | parent=parent, 58 | google_ads_link=GoogleAdsLink( 59 | customer_id=customer_id, 60 | ads_personalization_enabled=row['ads_personalization_enabled'])) 61 | responses.append({ 62 | 'ga_propety_id': row['ga4_property_id'], 63 | 'platform_id': customer_id, 64 | 'type': 'ads', 65 | 'link_resource_name': response.name, 66 | 'result': 'created'}) 67 | except Exception as e: 68 | responses.append({ 69 | 'ga_propety_id': row['ga4_property_id'], 70 | 'platform_id': customer_id, 71 | 'type': 'ads', 72 | 'link_resource_name': 'n/a', 73 | 'result': e}) 74 | time.sleep(REQUEST_DELAY) 75 | if row['request_type'] == 'dv360': 76 | try: 77 | advertiser_id = str(row['dv360_advertiser_id']) 78 | parent = f"properties/{row['ga4_property_id']}" 79 | dv360_link = DisplayVideo360AdvertiserLink( 80 | advertiser_id=advertiser_id, 81 | ads_personalization_enabled = row['ads_personalization_enabled'], 82 | campaign_data_sharing_enabled = row['dv360_campaign_data_sharing_enabled'], 83 | cost_data_sharing_enabled = row['dv360_cost_data_sharing_enabled']) 84 | response = ga_client.create_display_video360_advertiser_link( 85 | parent=parent, 86 | display_video360_advertiser_link=dv360_link) 87 | responses.append({ 88 | 'ga_propety_id': row['ga4_property_id'], 89 | 'platform_id': advertiser_id, 90 | 'type': 'dv360', 91 | 'link_resource_name': response.name, 92 | 'result': 'created'}) 93 | except Exception as e: 94 | responses.append({ 95 | 'ga_propety_id': row['ga4_property_id'], 96 | 'platform_id': advertiers_id, 97 | 'type': 'dv360', 98 | 'link_resource_name': 'n/a', 99 | 'result': e}) 100 | time.sleep(REQUEST_DELAY) 101 | if row['request_type'] == 'dv360_link_proposal': 102 | try: 103 | advertiser_id = str(row['dv360_advertiser_id']) 104 | parent = f"properties/{row['ga4_property_id']}" 105 | dv360_link_proposal = DisplayVideo360AdvertiserLinkProposal( 106 | advertiser_id=advertiser_id, 107 | ads_personalization_enabled = row['ads_personalization_enabled'], 108 | campaign_data_sharing_enabled = row['dv360_campaign_data_sharing_enabled'], 109 | cost_data_sharing_enabled = row['dv360_cost_data_sharing_enabled'], 110 | validation_email = row['dv360_proposal_validation_email']) 111 | response = ga_client.create_display_video360_advertiser_link( 112 | parent=parent, 113 | display_video360_advertiser_link_proposal=dv360_link_proposal) 114 | responses.append({ 115 | 'ga_propety_id': row['ga4_property_id'], 116 | 'platform_id': advertiser_id, 117 | 'type': 'dv360 link proposal', 118 | 'link_resource_name': response.name, 119 | 'result': 'created'}) 120 | except Exception as e: 121 | responses.append({ 122 | 'ga_propety_id': row['ga4_property_id'], 123 | 'platform_id': advertiers_id, 124 | 'type': 'dv360 link proposal', 125 | 'link_resource_name': 'n/a', 126 | 'result': e}) 127 | time.sleep(REQUEST_DELAY) 128 | else: 129 | current_row = index + 1 130 | remaining = df.iloc[current_row:,:] 131 | remaining_csv = pandas.DataFrame(remaining).to_csv() 132 | remaining_blob = storage_client.get_bucket(bucket).blob(f'remaining-{time.time()}.csv') 133 | remaining_blob.upload_from_string(remaining_csv, content_type='text/csv') 134 | break; 135 | csv_str = pandas.DataFrame(responses).to_csv() 136 | output_bucket_blob = storage_client.get_bucket(OUTPUT_BUCKET).blob(f'results-{time.time()}.csv') 137 | output_bucket_blob.upload_from_string(csv_str, content_type='text/csv') 138 | 139 | 140 | 141 | def get_ga_client(): 142 | credentials = google.oauth2.credentials.Credentials( 143 | ACCESS_TOKEN, 144 | refresh_token=REFRESH_TOKEN, 145 | token_uri=TOKEN_URI, 146 | client_id=CLIENT_ID, 147 | client_secret=CLIENT_SECRET) 148 | return AnalyticsAdminServiceClient(credentials=credentials) 149 | -------------------------------------------------------------------------------- /cloud/linker/requirements.txt: -------------------------------------------------------------------------------- 1 | functions-framework==3.* 2 | google-auth-oauthlib 3 | google-analytics-admin 4 | pandas 5 | google-cloud-storage 6 | fsspec 7 | gcsfs 8 | -------------------------------------------------------------------------------- /cloud/message_publisher/message_publisher.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Loops through the uploaded CSV file to publish pub/sub messages. 16 | 17 | Parses uploaded CSV files to publish pub/sub messages that contain data to 18 | eventually create Google Analytics resources. 19 | 20 | """ 21 | 22 | import os 23 | import json 24 | import time 25 | import pandas 26 | import functions_framework 27 | from enum import Enum 28 | from typing import Dict 29 | from google.cloud import storage 30 | from google.cloud import pubsub_v1 31 | from cloudevents.http import CloudEvent 32 | 33 | class ResourceType(Enum): 34 | ADS_LINK = 'ads_link' 35 | DV360_LINK = 'dv360_link' 36 | DV360_LINK_PROPOSAL = 'dv360_link_proposal' 37 | 38 | # Set to slightly less than 1 hour so that the function can upload 39 | # a new CSV to the input bucket if it is close to timing out. 40 | TIMEOUT = 3500 41 | 42 | @functions_framework.cloud_event 43 | def main(cloud_event: CloudEvent) -> str: 44 | """Gets the CSV file uploaded to the specified Cloud Storage bucket and 45 | loops through the contents. Each row in the file should cause a pub/sub 46 | message to be published containing data to create a Google Analytics 47 | resource. 48 | 49 | Args: 50 | cloud_event: The event containing meta information about the file 51 | uploaded to the Cloud Storage bucket. 52 | 53 | Returns: 54 | An string indicating whether or not the function has invoked another 55 | instance of itself by uploading the remaining CSV file values. 56 | """ 57 | start_time = time.time() 58 | bucket = cloud_event.data['bucket'] 59 | name = cloud_event.data['name'] 60 | file_name = 'gs://' + bucket + '/' + name 61 | df = pandas.read_csv(file_name, on_bad_lines='skip') 62 | records = df.to_dict('records') 63 | enable_logging = str_to_bool(os.environ.get('ENABLE_LOGGING')) 64 | event_data = { 65 | 'client_id': os.environ.get('CLIENT_ID'), 66 | 'client_secret': os.environ.get('CLIENT_SECRET'), 67 | 'refresh_token': os.environ.get('REFRESH_TOKEN'), 68 | 'token_uri': os.environ.get('TOKEN_URI'), 69 | 'scopes': ['https://www.googleapis.com/auth/analytics.edit'], 70 | 'enable_logging': enable_logging} 71 | for index, row in enumerate(records): 72 | if time.time() - start_time < TIMEOUT: 73 | for resource in ResourceType: 74 | if resource.value in name: 75 | topic = f'projects/{os.environ.get("PROJECT_ID")}/topics/ga_{resource.value}' 76 | encoded_data = json.dumps(event_data | row).encode('utf-8') 77 | pubsub_v1.PublisherClient().publish(topic, encoded_data) 78 | time.sleep(.2) 79 | else: 80 | current_row = index + 1 81 | remaining = df.iloc[current_row:,:] 82 | remaining_csv = pandas.DataFrame(remaining).to_csv() 83 | remaining_blob = storage.Client().get_bucket(bucket).blob( 84 | f'remaining-{time.time()}.csv') 85 | remaining_blob.upload_from_string( 86 | remaining_csv, content_type='text/csv') 87 | if (enable_logging): 88 | print('continuing') 89 | return 'continuing' 90 | if (enable_logging): 91 | print('done') 92 | return 'done' 93 | 94 | 95 | def str_to_bool(value: str|None): 96 | """Converts the string "true" to the boolean type. Everything else is 97 | returned as False. 98 | 99 | Args: 100 | value: The value to be converted to a boolean. 101 | 102 | Returns: 103 | Either True or False as boolean types. 104 | """ 105 | if value.strip().lower() == 'true': 106 | return True 107 | else: 108 | return False 109 | -------------------------------------------------------------------------------- /cloud/message_publisher/requirements.txt: -------------------------------------------------------------------------------- 1 | functions-framework==3.* 2 | google-cloud-pubsub 3 | google-cloud-storage 4 | cloudevents 5 | pandas 6 | fsspec 7 | gcsfs 8 | -------------------------------------------------------------------------------- /docs/code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, education, socio-economic status, nationality, personal appearance, 10 | race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | This Code of Conduct also applies outside the project spaces when the Project 56 | Steward has a reasonable belief that an individual's behavior may have a 57 | negative impact on the project or its community. 58 | 59 | ## Conflict Resolution 60 | 61 | We do not believe that all conflict is bad; healthy debate and disagreement 62 | often yield positive results. However, it is never okay to be disrespectful or 63 | to engage in behavior that violates the project’s code of conduct. 64 | 65 | If you see someone violating the code of conduct, you are encouraged to address 66 | the behavior directly with those involved. Many issues can be resolved quickly 67 | and easily, and this gives people more control over the outcome of their 68 | dispute. If you are unable to resolve the matter for any reason, or if the 69 | behavior is threatening or harassing, report it. We are dedicated to providing 70 | an environment where participants feel welcome and safe. 71 | 72 | Reports should be directed to *[PROJECT STEWARD NAME(s) AND EMAIL(s)]*, the 73 | Project Steward(s) for *[PROJECT NAME]*. It is the Project Steward’s duty to 74 | receive and address reported violations of the code of conduct. They will then 75 | work with a committee consisting of representatives from the Open Source 76 | Programs Office and the Google Open Source Strategy team. If for any reason you 77 | are uncomfortable reaching out to the Project Steward, please email 78 | opensource@google.com. 79 | 80 | We will investigate every complaint, but you may not receive a direct response. 81 | We will use our discretion in determining when and how to follow up on reported 82 | incidents, which may range from not taking action to permanent expulsion from 83 | the project and project-sponsored spaces. We will notify the accused of the 84 | report and provide them an opportunity to discuss it before any action is taken. 85 | The identity of the reporter will be omitted from the details of the report 86 | supplied to the accused. In potentially harmful situations, such as ongoing 87 | harassment or threats to anyone's safety, we may take action without notice. 88 | 89 | ## Attribution 90 | 91 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, 92 | available at 93 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 94 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code Reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google/conduct/). 29 | -------------------------------------------------------------------------------- /new_analytics/adSenseLinks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Retrieves the AdSense links for a given set of properties. 19 | * @param {!Array} properties A two dimensional array of 20 | * account and property names and ids. 21 | * @return {!Array} A two dimensional array where each 22 | * array contains metadata about the AdSense links for the given 23 | * set of properties. 24 | */ 25 | function listGA4AdSenseLinks(properties) { 26 | let sheetValuesArray = []; 27 | properties.forEach(property => { 28 | const propertyName = 'properties/' + property[3]; 29 | const links = listGA4Entities('adSenseLinks', propertyName).links; 30 | if (links != undefined) { 31 | links.forEach(link => { 32 | sheetValuesArray.push([ 33 | property[0], 34 | property[1], 35 | property[2], 36 | property[3], 37 | link.adClientCode, 38 | link.name 39 | ]); 40 | }); 41 | } 42 | }); 43 | return sheetValuesArray; 44 | } 45 | 46 | /** 47 | * Writes GA4 AdSense links to a sheet. 48 | */ 49 | function writeGA4AdSenseLinksToSheet() { 50 | const selectedProperties = getSelectedGa4Properties(); 51 | const channelGroups = listGA4AdSenseLinks(selectedProperties); 52 | clearSheetContent(sheetsMeta.ga4.adSenseLinks); 53 | writeToSheet(channelGroups, sheetsMeta.ga4.adSenseLinks.sheetName); 54 | } -------------------------------------------------------------------------------- /new_analytics/analyticsAdminApi.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const ga4RequestDelay = 100; 18 | const ga4Resource = { 19 | accountSummaries: AnalyticsAdmin.AccountSummaries, 20 | accounts: AnalyticsAdmin.Accounts, 21 | streams: AnalyticsAdmin.Properties.DataStreams, 22 | firebaseLinks: AnalyticsAdmin.Properties.FirebaseLinks, 23 | googleAdsLinks: AnalyticsAdmin.Properties.GoogleAdsLinks, 24 | customDimensions: AnalyticsAdmin.Properties.CustomDimensions, 25 | customMetrics: AnalyticsAdmin.Properties.CustomMetrics, 26 | keyEvents: AnalyticsAdmin.Properties.KeyEvents, 27 | displayVideo360AdvertiserLinks: AnalyticsAdmin.Properties.DisplayVideo360AdvertiserLinks, 28 | properties: AnalyticsAdmin.Properties, 29 | audiences: AnalyticsAdmin.Properties.Audiences, 30 | accountAccessBindings: AnalyticsAdmin.Accounts.AccessBindings, 31 | propertyAccessBindings: AnalyticsAdmin.Properties.AccessBindings, 32 | searchAds360Links: AnalyticsAdmin.Properties.SearchAds360Links, 33 | bigqueryLinks: AnalyticsAdmin.Properties.BigQueryLinks, 34 | expandedDataSets: AnalyticsAdmin.Properties.ExpandedDataSets, 35 | channelGroups: AnalyticsAdmin.Properties.ChannelGroups, 36 | measurementProtocolSecrets: AnalyticsAdmin.Properties.DataStreams.MeasurementProtocolSecrets, 37 | adSenseLinks: AnalyticsAdmin.Properties.AdSenseLinks, 38 | eventCreateRules: AnalyticsAdmin.Properties.DataStreams.EventCreateRules, 39 | eventEditRules: AnalyticsAdmin.Properties.DataStreams.EventEditRules, 40 | subpropertyEventFilters: AnalyticsAdmin.Properties.SubpropertyEventFilters, 41 | rollupPropertySourceLinks: AnalyticsAdmin.Properties.RollupPropertySourceLinks, 42 | calculatedMetrics: AnalyticsAdmin.Properties.CalculatedMetrics 43 | }; 44 | 45 | /** 46 | * Gets a resource. 47 | * @param {string} resourceKey The GA4 entity being requested. 48 | * @param {string} parent 49 | * @return {!Object} Either a response from the API or an error message. 50 | */ 51 | function getGA4Resource(resourceKey, parent) { 52 | try { 53 | let response = {}; 54 | if (resourceKey == 'enhancedMeasurementSettings') { 55 | response = ga4Resource.streams.getEnhancedMeasurementSettings(parent); 56 | } else if (resourceKey == 'attributionSettings') { 57 | response = ga4Resource.properties.getAttributionSettings(parent); 58 | } else if (resourceKey == 'dataRetentionSettings') { 59 | response = ga4Resource.properties.getDataRetentionSettings(parent); 60 | } else if (resourceKey == 'googleSignalsSettings') { 61 | response = ga4Resource.properties.getGoogleSignalsSettings(parent); 62 | } else { 63 | response = ga4Resource[resourceKey].get(parent); 64 | } 65 | Utilities.sleep(ga4RequestDelay); 66 | return response; 67 | } catch(e) { 68 | console.log(e); 69 | return e; 70 | } 71 | } 72 | 73 | /** 74 | * Lists most GA4 entities. 75 | * @param {string} resourceKey The GA4 entity being requested. 76 | * @param {string} parent 77 | * @return {!Object} Either a response from the API or an error message. 78 | */ 79 | function listGA4Entities(resourceKey, parent) { 80 | try { 81 | let items = resourceKey; 82 | const options = {pageSize: 200}; 83 | let response = {}; 84 | if (parent != undefined) { 85 | parent.pageSize = 200; 86 | if (resourceKey == 'properties') { 87 | response = ga4Resource[resourceKey].list(parent); 88 | } else if (resourceKey == 'accountAccessBindings' || 89 | resourceKey == 'propertyAccessBindings') { 90 | items = 'accessBindings'; 91 | response = ga4Resource[resourceKey].list(parent, options); 92 | } else if (resourceKey == 'connectedSiteTags') { 93 | delete parent.pageSize; 94 | response = ga4Resource.properties.listConnectedSiteTags(parent); 95 | } else { 96 | response = ga4Resource[resourceKey].list(parent, options); 97 | } 98 | } else { 99 | response = ga4Resource[resourceKey].list(options); 100 | } 101 | options.pageToken = response.nextPageToken; 102 | Utilities.sleep(ga4RequestDelay); 103 | while (options.pageToken != undefined) { 104 | let nextPageResponse = {}; 105 | if (parent) { 106 | if (resourceKey == 'properties') { 107 | parent.pageSize = 200; 108 | parent.pageToken = options.pageToken; 109 | nextPageResponse = ga4Resource[resourceKey].list(parent); 110 | } else if (resourceKey == 'accountAccessBindings' || 111 | resourceKey == 'propertyAccessBindings') { 112 | items = 'accessBindings'; 113 | nextPageResponse = ga4Resource[resourceKey].list(parent, options); 114 | } else if (resourceKey == 'connectedSiteTags') { 115 | delete parent.pageSize; 116 | nextPageResponse = ga4Resource 117 | .properties.listConnectedSiteTags(parent); 118 | } else { 119 | nextPageResponse = ga4Resource[resourceKey].list(parent, options); 120 | } 121 | } else { 122 | nextPageResponse = ga4Resource[resourceKey].list(options); 123 | } 124 | response[items] = response[items].concat(nextPageResponse[items]); 125 | options.pageToken = nextPageResponse.nextPageToken; 126 | Utilities.sleep(ga4RequestDelay); 127 | } 128 | return response; 129 | } catch(e) { 130 | console.log(e); 131 | return e; 132 | } 133 | } 134 | 135 | function archiveGA4CustomDefinition(resourceKey, resourceName) { 136 | try { 137 | const response = ga4Resource[resourceKey].archive({}, resourceName); 138 | Utilities.sleep(ga4RequestDelay); 139 | return response; 140 | } catch(e) { 141 | console.log(e); 142 | return e; 143 | } 144 | } 145 | 146 | function createGA4Entity(resourceKey, name, payload) { 147 | try { 148 | let response = {}; 149 | if (resourceKey == 'properties') { 150 | payload.parent = name; 151 | response = ga4Resource[resourceKey].create(payload); 152 | } else if (resourceKey == 'connectedSiteTags') { 153 | response = ga4Resource.properties.createConnectedSiteTag(payload); 154 | } else if (resourceKey == 'subproperties') { 155 | response = ga4Resource.properties.provisionSubproperty(payload); 156 | } else if (resourceKey == 'rollupProperties') { 157 | response = ga4Resource.properties.createRollupProperty(payload); 158 | } else { 159 | response = ga4Resource[resourceKey].create(payload, name); 160 | } 161 | Utilities.sleep(ga4RequestDelay); 162 | return response; 163 | } catch(e) { 164 | console.log(e); 165 | return e; 166 | } 167 | } 168 | 169 | function deleteGA4Entity(resourceKey, name) { 170 | try { 171 | let response = {}; 172 | if (resourceKey == 'connectedSiteTags') { 173 | response = ga4Resource.properties.deleteConnectedSiteTag(name); 174 | } else { 175 | response = ga4Resource[resourceKey].remove(name); 176 | } 177 | Utilities.sleep(ga4RequestDelay); 178 | return response; 179 | } catch(e) { 180 | console.log(e); 181 | return e; 182 | } 183 | } 184 | 185 | function updateGA4Entity(resourceKey, name, payload) { 186 | try { 187 | let mask = ''; 188 | let response = {}; 189 | if (resourceKey == 'customMetrics') { 190 | if (payload.measurementUnit == 'CURRENCY') { 191 | delete payload.measurementUnit; 192 | mask = 'displayName,description,restrictedMetricType'; 193 | } else { 194 | mask = 'displayName,measurementUnit,description,restrictedMetricType'; 195 | } 196 | } else if (resourceKey == 'audiences') { 197 | for (field in payload) { 198 | mask += field + ',' 199 | } 200 | } else if (resourceKey == 'attributionSettings') { 201 | mask = 'acquisitionConversionEventLookbackWindow,otherConversionEventLookbackWindow,reportingAttributionModel'; 202 | } else { 203 | mask = '*'; 204 | } 205 | if (resourceKey == 'accountAccessBindings' || 206 | resourceKey == 'propertyAccessBindings') { 207 | response = ga4Resource[resourceKey].patch(payload, name); 208 | } else if (resourceKey = 'dataRetentionSettings') { 209 | response = ga4Resource.properties.updateDataRetentionSettings( 210 | payload, name, {updateMask: mask} 211 | ); 212 | } else if (resourceKey = 'attributionSettings') { 213 | response = ga4Resource.properties.updateAttributionSettings( 214 | payload, name, {updateMask: mask} 215 | ); 216 | } else if (resourceKey = 'enhancedMeasurementSettings') { 217 | response = ga4Resource.streams.updateEnhancedMeasurementSettings( 218 | payload, name, {updateMask: mask} 219 | ); 220 | } else { 221 | response = ga4Resource[resourceKey].patch( 222 | payload, name, {updateMask: mask}); 223 | } 224 | Utilities.sleep(ga4RequestDelay); 225 | return response; 226 | } catch(e) { 227 | console.log(e); 228 | return e; 229 | } 230 | } 231 | 232 | /** 233 | * Updates the data retention settings for a property. 234 | * @param {string} evenDataRetention The length of time data should be retained. 235 | * @param {boolean} resetUserDataOnNewActivity Whether or not user data should 236 | * be reset on new activity. 237 | * @param {string} parent The parent data stream path. 238 | * @returns {!Object} Either the updated data retention setting or an error. 239 | */ 240 | function updateDataRetentionSettings( 241 | evenDataRetention, resetUserDataOnNewActivity, parent) { 242 | try { 243 | let response = ''; 244 | response = AnalyticsAdmin.Properties.updateDataRetentionSettings({ 245 | eventDataRetention: evenDataRetention, 246 | resetUserDataOnNewActivity: resetUserDataOnNewActivity 247 | }, parent, {updateMask: '*'}); 248 | Utilities.sleep(ga4RequestDelay); 249 | return response; 250 | } catch(e) { 251 | console.log(e); 252 | return e; 253 | } 254 | } 255 | 256 | /** 257 | * Updates the enhanced measurement settings for a datastream. 258 | * @param {!Object} settings The new setting values. 259 | * @param {string} parent The parent data stream path. 260 | * @returns {!Object} Either the updated enhanced measument settings or an error. 261 | */ 262 | function updateEnhancedMeasurementSettings(settings, parent) { 263 | try { 264 | let response = ''; 265 | response = AnalyticsAdmin.Properties.DataStreams 266 | .updateEnhancedMeasurementSettings(settings, parent, {updateMask: '*'}); 267 | Utilities.sleep(ga4RequestDelay); 268 | return response; 269 | } catch(e) { 270 | console.log(e); 271 | return e; 272 | } 273 | } 274 | 275 | -------------------------------------------------------------------------------- /new_analytics/channelGroups.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Retrieves the channel groups for a given set of properties. 19 | * @param {!Array} properties A two dimensional array of 20 | * account and property names and ids. 21 | * @return {!Array} A two dimensional array where each 22 | * array contains metadata about the channel groups for the given 23 | * set of properties. 24 | */ 25 | function listGA4ChannelGroups(properties) { 26 | let sheetValuesArray = []; 27 | properties.forEach(property => { 28 | const propertyName = 'properties/' + property[3]; 29 | const channelGroups = listGA4Entities( 30 | 'channelGroups', propertyName).channelGroups; 31 | if (channelGroups != undefined) { 32 | channelGroups.forEach(group => { 33 | sheetValuesArray.push([ 34 | property[0], 35 | property[1], 36 | property[2], 37 | property[3], 38 | group.displayName || 'Default Channel Group', 39 | group.name, 40 | group.description, 41 | group.systemDefined, 42 | group.primary, 43 | JSON.stringify(group.groupingRule) || '[]' 44 | ]); 45 | }); 46 | } 47 | }); 48 | return sheetValuesArray; 49 | } 50 | 51 | /** 52 | * Writes GA4 channel groups information to a sheet. 53 | */ 54 | function writeGA4ChannelGroupsToSheet() { 55 | const selectedProperties = getSelectedGa4Properties(); 56 | const channelGroups = listGA4ChannelGroups(selectedProperties); 57 | clearSheetContent(sheetsMeta.ga4.channelGroups); 58 | writeToSheet(channelGroups, sheetsMeta.ga4.channelGroups.sheetName); 59 | } -------------------------------------------------------------------------------- /new_analytics/easyPropertyCreation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * 19 | */ 20 | function isEmptyObject(obj) { 21 | if (obj) { 22 | return Object.keys(obj).length === 0; 23 | } else { 24 | true; 25 | } 26 | } 27 | 28 | /** 29 | * Gets the GA4 settings for the features for an array of properties and returns 30 | * a double array. 31 | * @param {!Array} properties 32 | */ 33 | function listAllFeatureSettings(properties) { 34 | const data = []; 35 | if (properties.length > 0) { 36 | properties.forEach(property => { 37 | const accountName = property[0]; 38 | const accountId = property[1]; 39 | const propertyName = property[2]; 40 | const propertyId = property[3]; 41 | const propertyResourceName = 'properties/' + propertyId; 42 | const attributionSettings = getGA4Resource( 43 | 'attributionSettings', 44 | `${propertyResourceName}/attributionSettings`); 45 | const dataRetentionSettings = getGA4Resource( 46 | 'dataRetentionSettings', 47 | `${propertyResourceName}/dataRetentionSettings`); 48 | const dataStreams = listGA4Entities( 49 | 'streams', propertyResourceName).dataStreams; 50 | if (dataStreams) { 51 | dataStreams.forEach(stream => { 52 | if (stream.webStreamData) { 53 | stream.webStreamData.enhancedMeasurementSettings = getGA4Resource( 54 | 'enhancedMeasurementSettings', 55 | `${stream.name}/enhancedMeasurementSettings`); 56 | } 57 | stream.measurementProtocolSecrets = listGA4Entities( 58 | 'measurementProtocolSecrets', stream.name 59 | ).measurementProtocolSecrets; 60 | stream.eventCreateRules = listGA4Entities( 61 | 'eventCreateRules', stream.name).eventCreateRules; 62 | }); 63 | } 64 | let audiencesExist = false; 65 | const audiences = listGA4Entities( 66 | 'audiences', propertyResourceName).audiences; 67 | if (audiences) { 68 | audiencesExist = true; 69 | } 70 | data.push([ 71 | accountName, 72 | accountId, 73 | propertyName, 74 | propertyId, 75 | '', '', 76 | JSON.stringify(cleanOutput('properties', 77 | getGA4Resource('properties', propertyResourceName)), null, 2), 78 | JSON.stringify(cleanOutput( 79 | 'data retention',dataRetentionSettings), null, 2), 80 | JSON.stringify(cleanOutput( 81 | 'attribution', attributionSettings), null, 2), 82 | audiencesExist, 83 | JSON.stringify(cleanOutput('streams', dataStreams), null, 2) || '[]', 84 | JSON.stringify(cleanOutput('customDimensions', 85 | listGA4Entities( 86 | 'customDimensions', 87 | propertyResourceName).customDimensions), null, 2) || '[]', 88 | JSON.stringify(cleanOutput('customMetrics', 89 | listGA4Entities( 90 | 'customMetrics', 91 | propertyResourceName).customMetrics), null, 2) || '[]', 92 | JSON.stringify(cleanOutput('conversionEvents', 93 | listGA4Entities( 94 | 'conversionEvents', 95 | propertyResourceName).conversionEvents), null, 2) || '[]', 96 | JSON.stringify(cleanOutput('googleAdsLinks', 97 | listGA4Entities( 98 | 'googleAdsLinks', 99 | propertyResourceName).googleAdsLinks), null, 2) || '[]', 100 | JSON.stringify(cleanOutput('displayVideo360AdvertiserLinks', 101 | listGA4Entities( 102 | 'displayVideo360AdvertiserLinks', 103 | propertyResourceName).displayVideo360AdvertiserLinks), 104 | null, 2) || '[]', 105 | JSON.stringify(cleanOutput('searchAds360Links', 106 | listGA4Entities( 107 | 'searchAds360Links', 108 | propertyResourceName).searchAds360Links), null, 2) || '[]', 109 | JSON.stringify(cleanOutput('firebaseLinks', 110 | listGA4Entities( 111 | 'firebaseLinks', 112 | propertyResourceName).firebaseLinks), null, 2) || '[]', 113 | JSON.stringify(cleanOutput('adSenseLinks', 114 | listGA4Entities( 115 | 'adSenseLinks', 116 | propertyResourceName).firebaseLinks), null, 2) || '[]', 117 | JSON.stringify(cleanOutput('channelGroups', 118 | listGA4Entities( 119 | 'channelGroups', 120 | propertyResourceName).channelGroups), null, 2) || '[]' 121 | ]); 122 | }); 123 | } 124 | return data; 125 | } 126 | 127 | /** 128 | * Removes unnecessary fields from the various resources. 129 | */ 130 | function cleanOutput(resourceType, value) { 131 | if (value) { 132 | if (resourceType == 'properties') { 133 | delete value.updateTime; 134 | delete value.createTime; 135 | delete value.displayName; 136 | delete value.account; 137 | delete value.parent; 138 | delete value.serviceLevel; 139 | delete value.name; 140 | delete value.propertyType; 141 | } else if (resourceType == 'data retention') { 142 | delete value.name; 143 | if (/twenty|thirty|fifty/.test(value.eventDataRetention)) { 144 | value.evenDataRetention = 'FOURTEEN_MONTHS'; 145 | } 146 | } else if (resourceType == 'attribution') { 147 | delete value.name; 148 | } else if (resourceType == 'audiences') { 149 | const defaultAudienceNames = ['All Users', 'Purchasers']; 150 | defaultAudienceNames.forEach(defaultName => { 151 | const index = value.findIndex(aud => aud.displayName == defaultName); 152 | if (index > -1) { 153 | value.splice(index, 1); 154 | } 155 | }); 156 | value.forEach(audience => { 157 | delete audience.name; 158 | if (audience.description == undefined) { 159 | audience.description = audience.displayName; 160 | } 161 | if (!audience.eventTrigger) { 162 | delete audience.eventTrigger; 163 | } 164 | }); 165 | } else if (resourceType == 'streams') { 166 | value.forEach(stream => { 167 | delete stream.name; 168 | delete stream.updateTime; 169 | delete stream.createTime; 170 | if (stream.eventCreateRules) { 171 | delete stream.eventCreateRules.name; 172 | } 173 | if (stream.measurementProtocolSecrets) { 174 | delete stream.measurementProtocolSecrets.name; 175 | delete stream.measurementProtocolSecrets.secretValue; 176 | } 177 | if (stream.webStreamData) { 178 | delete stream.webStreamData.measurementId; 179 | delete stream.webStreamData.enhancedMeasurementSettings.name; 180 | } 181 | }); 182 | } else if (resourceType == 'customDimensions') { 183 | value.forEach(dimension => { 184 | delete dimension.name; 185 | }); 186 | } else if (resourceType == 'customMetrics') { 187 | value.forEach(metric => { 188 | delete metric.name; 189 | }); 190 | } else if (resourceType == 'conversionEvents') { 191 | const defaultConversions = [ 192 | 'app_store_subscription_convert', 193 | 'app_store_subscription_renew', 194 | 'ecommerce_purchase', 195 | 'first_open', 196 | 'in_app_purchase', 197 | 'purchase' 198 | ]; 199 | defaultConversions.forEach(defaultConversion => { 200 | const index = value.findIndex( 201 | conversion => conversion.eventName == defaultConversion); 202 | if (index > -1) { 203 | value.splice(index, 1); 204 | } 205 | }); 206 | if (value != undefined) { 207 | value.forEach(conversion => { 208 | delete conversion.name; 209 | delete conversion.createTime; 210 | delete conversion.deletable; 211 | }); 212 | } 213 | } else if (resourceType == 'googleAdsLinks') { 214 | value.forEach(link => { 215 | delete link.name; 216 | delete link.canManageClients; 217 | delete link.createTime; 218 | delete link.updateTime; 219 | delete link.creatorEmailAddress; 220 | }); 221 | } else if (resourceType == 'displayVideo360AdvertiserLinks') { 222 | value.forEach(link => { 223 | delete link.name; 224 | delete link.advertiserDisplayName; 225 | }); 226 | } else if (resourceType == 'searchAds360Links') { 227 | value.forEach(link => { 228 | delete link.name; 229 | delete link.advertiserDisplayName; 230 | }); 231 | } else if (resourceType == 'firebaseLinks') { 232 | value.forEach(link => { 233 | delete link.name; 234 | delete link.createTime; 235 | }); 236 | } else if (resourceType == 'channelGroups') { 237 | value = value.filter(group => !group.systemDefined); 238 | value.forEach(group => { 239 | delete group.name; 240 | delete group.systemDefined; 241 | }); 242 | } 243 | return value; 244 | } 245 | } 246 | 247 | /** 248 | * Create new property. 249 | */ 250 | function createPropertiesFromTemplates() { 251 | const data = 252 | getDataFromSheet(sheetsMeta.ga4.fullPropertyDeployment.sheetName); 253 | if (data.length > 0) { 254 | data.forEach((row, index) => { 255 | const create = row[row.length - 4]; 256 | if (create) { 257 | const parentAccount = 'accounts/' + row[4]; 258 | 259 | // Parse settings. 260 | const settings = { 261 | property: JSON.parse(row[6] || '[]'), 262 | dataRetentionSettings: JSON.parse(row[7] || '[]'), 263 | attributionSettings: JSON.parse(row[8] || '[]'), 264 | audiences: row[9], 265 | streams: JSON.parse(row[10] || '[]'), 266 | customDimensions: JSON.parse(row[11] || '[]'), 267 | customMetrics: JSON.parse(row[12] || '[]'), 268 | conversionEvents: JSON.parse(row[13] || '[]'), 269 | googleAdsLinks: JSON.parse(row[14] || '[]'), 270 | displayVideo360AdvertiserLinks: JSON.parse(row[15] || '[]'), 271 | searchAds360Links: JSON.parse(row[16] || '[]'), 272 | firebaseLinks: JSON.parse(row[17] || '[]'), 273 | adSenseLinks: JSON.parse(row[18] || '[]'), 274 | channelGroups: JSON.parse(row[19] || '[]'), 275 | }; 276 | 277 | let newProperty = null; 278 | let responses = []; 279 | for (setting in settings) { 280 | originalPropertyId = row[3]; 281 | if (setting == 'property') { 282 | // Create new property. 283 | settings.property.displayName = row[5]; 284 | newProperty = createGA4Entity( 285 | 'properties', parentAccount, settings.property); 286 | responses.push(newProperty); 287 | } else if (setting == 'dataRetentionSettings') { 288 | // Set data retention settings. 289 | const response = updateGA4Entity( 290 | setting, 291 | `${newProperty.name}/${setting}`, 292 | settings[setting] 293 | ); 294 | responses.push(response); 295 | } else if (setting == 'attributionSettings') { 296 | // Set attribution settings. 297 | const response = updateGA4Entity( 298 | setting, 299 | `${newProperty.name}/${setting}`, 300 | settings[setting] 301 | ); 302 | responses.push(response); 303 | } else { 304 | // Create settings. 305 | const values = settings[setting]; 306 | if (values.length > 0 && setting != 'audiences') { 307 | values.forEach(value => { 308 | if (value.webStreamData || 309 | value.androidAppStreamData || 310 | value.iosAppStreamData) { 311 | const mps = value.measurementProtocolSecrets; 312 | delete value.measurementProtocolSecrets; 313 | const ecr = value.eventCreateRules; 314 | delete value.eventCreateRules; 315 | let newStream = {}; 316 | if (value.webStreamData) { 317 | const ems = value.webStreamData.enhancedMeasurementSettings; 318 | delete value.webStreamData.enhancedMeasurementSettings; 319 | newStream = createGA4Entity( 320 | setting, newProperty.name, value); 321 | responses.push(newStream); 322 | const newEnhancedMeasurementSettings = updateGA4Entity( 323 | 'enhancedMeasurementSettings', 324 | `${newStream.name}/enhancedMeasurementSettings`, 325 | JSON.parse(JSON.stringify(ems)) 326 | ); 327 | responses.push(newEnhancedMeasurementSettings); 328 | } 329 | if (value.androidAppStreamData || value.iosAppStreamData) { 330 | newStream = createGA4Entity( 331 | setting, newProperty.name, value); 332 | } 333 | if (mps) { 334 | mps.forEach(secret => { 335 | const newSecret = createGA4Entity( 336 | 'measurementProtocolSecrets', newStream.name, secret); 337 | responses.push(newSecret); 338 | }); 339 | } 340 | if (ecr) { 341 | ecr.forEach(rule => { 342 | const newEventCreateRule = createGA4Entity( 343 | 'eventCreateRules', newStream.name, rule); 344 | responses.push(newEventCreateRule); 345 | }); 346 | } 347 | } else { 348 | const response = createGA4Entity( 349 | setting, newProperty.name, value); 350 | responses.push(response); 351 | } 352 | }); 353 | } else if (setting == 'audiences') { 354 | const copyAudiences = values; 355 | if (copyAudiences) { 356 | const audiences = listGA4Entities( 357 | setting, `properties/${originalPropertyId}`)[setting]; 358 | const templateValues = cleanOutput('audiences', audiences); 359 | templateValues.forEach(resource => { 360 | const response = createGA4Entity( 361 | setting, newProperty.name, 362 | JSON.parse(JSON.stringify(resource))); 363 | responses.push(response); 364 | }); 365 | } 366 | } 367 | } 368 | } 369 | const sheet = SpreadsheetApp.getActive().getSheetByName( 370 | sheetsMeta.ga4.fullPropertyDeployment.sheetName 371 | ); 372 | // Set new property URL in the spreadsheet. 373 | sheet.getRange( 374 | index + 2, 375 | sheetsMeta.ga4.fullPropertyDeployment.read.numColumns - 4, 1, 1) 376 | .setFormula( 377 | '=HYPERLINK("https://analytics.google.com/analytics/web/#/p' + 378 | newProperty.name.split('/')[1] + '", "New Property")'); 379 | // Write the actions taken. 380 | writeActionTakenToSheet( 381 | sheetsMeta.ga4.fullPropertyDeployment.sheetName, index, 382 | responseCheck(responses, 'create')); 383 | } 384 | }); 385 | } 386 | } 387 | 388 | 389 | /** 390 | * Writes all of property deployment templates to a spreadsheet. 391 | */ 392 | function writePropertyTemplatesToSheet() { 393 | const properties = getSelectedGa4Properties(); 394 | const data = listAllFeatureSettings(properties); 395 | clearSheetContent(sheetsMeta.ga4.fullPropertyDeployment); 396 | if (data.length > 0) { 397 | writeToSheet(data, sheetsMeta.ga4.fullPropertyDeployment.sheetName); 398 | resizeEasyPropertyCreationSheetRowHeights(); 399 | } 400 | } 401 | 402 | /** 403 | * Resizes the row heights for the easy property creation sheet. 404 | */ 405 | function resizeEasyPropertyCreationSheetRowHeights() { 406 | resizeRowHeights(sheetsMeta.ga4.fullPropertyDeployment.sheetName, 50); 407 | } -------------------------------------------------------------------------------- /new_analytics/eventCreateRules.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Retrieves the event create rules for a given set of properties. 19 | * @param {!Array} properties A two dimensional array of 20 | * account and property names and ids. 21 | * @return {!Array} A two dimensional array where each 22 | * array contains metadata about the event create rules for the given 23 | * set of properties. 24 | */ 25 | function listGA4EventCreateRules(streams) { 26 | let sheetValuesArray = []; 27 | streams.forEach(stream => { 28 | const streamName = `properties/${stream[3]}/dataStreams/${stream[5]}`; 29 | const eventCreateRules = listGA4Entities( 30 | 'eventCreateRules', streamName).eventCreateRules; 31 | if (eventCreateRules != undefined) { 32 | eventCreateRules.forEach(rule => { 33 | sheetValuesArray.push([ 34 | ...stream.slice(0, 6), 35 | rule.destinationEvent, 36 | rule.name, 37 | rule.sourceCopyParameters, 38 | JSON.stringify(rule.eventConditions) || '[]', 39 | JSON.stringify(rule.parameterMutations) || '[]' 40 | ]); 41 | }); 42 | } 43 | }); 44 | return sheetValuesArray; 45 | } 46 | 47 | /** 48 | * Writes GA4 event create rules to a sheet. 49 | */ 50 | function writeGA4EventCreateRulesToSheet() { 51 | const selectedStreams = getSelectedGa4DataStreams(); 52 | const mps = listGA4EventCreateRules(selectedStreams); 53 | clearSheetContent(sheetsMeta.ga4.eventCreateRules); 54 | writeToSheet(mps, sheetsMeta.ga4.eventCreateRules.sheetName); 55 | } -------------------------------------------------------------------------------- /new_analytics/eventEditRules.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Retrieves the event edit rules for a given set of properties. 19 | * @param {!Array} streams A two dimensional array of 20 | * account and stream names and ids. 21 | * @return {!Array} A two dimensional array where each 22 | * array contains metadata about the event edit rules for the given 23 | * set of properties. 24 | */ 25 | function listGA4EventEditRules(streams) { 26 | let sheetValuesArray = []; 27 | streams.forEach(stream => { 28 | const streamName = `properties/${stream[3]}/dataStreams/${stream[5]}`; 29 | const eventEditRules = listGA4Entities( 30 | 'eventEditRules', streamName).eventEditRules; 31 | if (eventEditRules != undefined) { 32 | eventEditRules.forEach(rule => { 33 | sheetValuesArray.push([ 34 | ...stream.slice(0, 6), 35 | rule.displayName, 36 | rule.name, 37 | rule.processingOrder, 38 | JSON.stringify(rule.eventConditions) || '[]', 39 | JSON.stringify(rule.parameterMutations) || '[]' 40 | ]); 41 | }); 42 | } 43 | }); 44 | return sheetValuesArray; 45 | } 46 | 47 | /** 48 | * Writes event edit rules to a sheet. 49 | */ 50 | function writeEventEditRulesToSheet() { 51 | const selectedStreams = getSelectedGa4DataStreams(); 52 | const eventEditRules = listGA4EventEditRules(selectedStreams); 53 | clearSheetContent(sheetsMeta.ga4.eventEditRules); 54 | writeToSheet(eventEditRules, sheetsMeta.ga4.eventEditRules.sheetName); 55 | } -------------------------------------------------------------------------------- /new_analytics/health_report.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Creates the GA4 health report by pulling information for each of the report 19 | * components and saving that datat to their respective sheets. The settings 20 | * data for both the aggregate slides and snapshot slides is then formatted and 21 | * looped through to build a new Google Slides presentation where placeholders 22 | * are replaced with new values. 23 | */ 24 | function createHealthReport() { 25 | if (SpreadsheetApp.getActive() 26 | .getSheetByName(sheetsMeta.ga4.healthReport.sheetName) 27 | .getRange('C8').getValue()) { 28 | listAllGA4PropertyResources(); 29 | } 30 | 31 | const healthReportSettings = formatHealthReportSettings(); 32 | 33 | const presentationTemplate = createNewPresentation( 34 | healthReportSettings.templateUrl, 35 | healthReportSettings.emails, 36 | healthReportSettings.access); 37 | presentationTemplate.setName(healthReportSettings.reportName); 38 | setReportListValue( 39 | healthReportSettings.reportName, presentationTemplate.getUrl()); 40 | 41 | if (healthReportSettings.templateInfo.length > 0) { 42 | healthReportSettings.templateInfo.forEach(templateInfo => { 43 | replaceSingleSlidePlaceholders( 44 | presentationTemplate, 45 | templateInfo, 46 | healthReportSettings.percentColorRanges); 47 | }); 48 | } 49 | createRepeatingSlides( 50 | presentationTemplate, 51 | healthReportSettings.percentColorRanges); 52 | } 53 | 54 | /** 55 | * Creates new Google Slides presentation and shares it based on provided 56 | * settings. 57 | * @param {string} templateUrl 58 | * @param {string} emailString 59 | * @param {string} access 60 | * @return {!Object} The presentation object. 61 | */ 62 | function createNewPresentation(templateUrl, emailString, access) { 63 | let presentationTemplate = ''; 64 | if (templateUrl == 'Default') { 65 | presentationTemplateId = DriveApp.getFileById( 66 | '1FikbCtbaU4BLqH8477wpFHCLDUMWalnnWW1VSchsR1s').makeCopy().getId(); 67 | presentationTemplate = SlidesApp.openById(presentationTemplateId); 68 | } else { 69 | presentationTemplateId = DriveApp.getFileById(templateUrl.split('/')[5]) 70 | .makeCopy().getId(); 71 | presentationTemplate = SlidesApp.openById(presentationTemplateId); 72 | } 73 | 74 | if (emailString.length > 0) { 75 | const emailArray = emailString.split(',').map(email => email.trim()); 76 | if (access == 'View') { 77 | presentationTemplate.addViewers(emailArray); 78 | } else if (access == 'Comment') { 79 | presentationTemplate.addCommenters(emailArray); 80 | } else if (access == 'Edit') { 81 | presentationTemplate.addEditors(emailArray); 82 | } 83 | } 84 | return presentationTemplate; 85 | } 86 | 87 | /** 88 | * Sets report URL and date created in the sheet. 89 | * @param {string} reportName 90 | * @param {string} reportUrl 91 | */ 92 | function setReportListValue(reportName, reportUrl) { 93 | const richTextValue = SpreadsheetApp.newRichTextValue().setText(reportName) 94 | .setLinkUrl(reportUrl).build(); 95 | const reportSheet = SpreadsheetApp.getActive() 96 | .getSheetByName(sheetsMeta.ga4.healthReport.sheetName); 97 | const reportRange = SpreadsheetApp.getActive() 98 | .getSheetByName(sheetsMeta.ga4.healthReport.sheetName) 99 | .getRange(3, 6, reportSheet.getLastRow(),1); 100 | const reportValues = reportRange.getValues(); 101 | let firstBlankCellIndex = reportValues.flat().indexOf(''); 102 | if (firstBlankCellIndex + 2 == reportSheet.getLastRow()) { 103 | reportSheet.insertRowAfter(reportSheet.getLastRow()); 104 | } 105 | reportSheet.getRange(firstBlankCellIndex + 3, 6, 1, 1) 106 | .setRichTextValue(richTextValue); 107 | reportSheet.getRange(firstBlankCellIndex + 3, 7, 1, 1).setValue(new Date()); 108 | } 109 | 110 | /** 111 | * Returns the name for a given field based on the supplied index. 112 | * @param {!number} index The index for setting. 113 | * @return {!string} The field name. 114 | */ 115 | function getSettingsKeyName(index) { 116 | switch (index) { 117 | case 0: 118 | return 'reportName'; 119 | case 1: 120 | return 'templateUrl'; 121 | case 2: 122 | return 'emails' 123 | case 3: 124 | return 'access' 125 | case 4: 126 | return 'pullData'; 127 | case 5: 128 | return 'percentColorRanges' 129 | case 6: 130 | return 'trueColor'; 131 | case 7: 132 | return 'falseColor'; 133 | } 134 | } 135 | 136 | /** 137 | * Retrieves data from the Report Settings sheet and formats it to be 138 | * useable when replacing values in the slides. 139 | * @returns {!Object} The health report settings object. 140 | */ 141 | function formatHealthReportSettings() { 142 | const settings = getDataFromSheet(sheetsMeta.ga4.healthReport.sheetName); 143 | const blankRowIndex = settings.findIndex(settingRow => settingRow[0] == ''); 144 | 145 | // Creates general settings. 146 | const generalRows = settings.slice(0, blankRowIndex); 147 | const generalSettings = generalRows.reduce((obj, settingRow, index) => { 148 | const key = getSettingsKeyName(index); 149 | const value = settingRow[2]; 150 | if (index != 5) { 151 | obj[key] = value; 152 | } else if (index == 5 && value.length > 0) { 153 | obj[key] = formatColorRangeObject(value); 154 | } 155 | return obj; 156 | }, {}); 157 | 158 | // Creates template settings. 159 | const templateRows = settings.slice(blankRowIndex + 2); 160 | const templateSettings = templateRows.reduce((obj, settingRow) => { 161 | // Starts constructing template objects. 162 | const templatePlaceholder = settingRow[0] 163 | const templateSlideId = settingRow[1]; 164 | const include = settingRow[2]; 165 | const templateValue = settingRow[3]; 166 | if (include) { 167 | const placeholderValuePair = {}; 168 | placeholderValuePair[templatePlaceholder] = templateValue; 169 | // Checks if there is no templateInfo field. 170 | if (obj.templateInfo == undefined) { 171 | // Creates the templateInfo field and adds the first object. 172 | obj.templateInfo = [{ 173 | templateSlideId: templateSlideId, 174 | templatePairs: [placeholderValuePair] 175 | }]; 176 | } else { 177 | // Checks if a template slide ID already exists in the templateInfo 178 | // object. 179 | if(obj.templateInfo.find( 180 | template => template.templateSlideId == templateSlideId)) { 181 | // Adds the new placeholder/value pair object to the templatePairs 182 | // array. 183 | const templateObj = obj.templateInfo.find( 184 | template => template.templateSlideId == templateSlideId); 185 | const existingPair = templateObj.templatePairs.find( 186 | pair => Object.keys(pair)[0] == templatePlaceholder); 187 | if (/list/.test(templatePlaceholder) && existingPair != undefined) { 188 | existingPair[templatePlaceholder] += (';;' + templateValue); 189 | } else { 190 | templateObj.templatePairs.push(placeholderValuePair); 191 | } 192 | } else { 193 | // Creates a new templateInfo object with the template slid ID and the 194 | // first placeholder/value pair. 195 | obj.templateInfo.push({ 196 | templateSlideId: templateSlideId, 197 | templatePairs: [placeholderValuePair] 198 | }); 199 | } 200 | } 201 | } 202 | return obj; 203 | }, {}); 204 | return {...generalSettings, ...templateSettings}; 205 | } 206 | 207 | /** 208 | * Creates the repeating slides, which are multiple slides within the larger 209 | * presentation that follow a set template. This process creates a new slide 210 | * based on a template slide and then replaces the placeholders with real values 211 | * in the new template slide copy. 212 | * @param {!Object} presentation 213 | * @param {!Array} percentColorRanges 214 | */ 215 | function createRepeatingSlides(presentation, percentColorRanges) { 216 | // Gets snapshot sheet. 217 | const snapshotSheet = SpreadsheetApp.getActiveSpreadsheet() 218 | .getSheetByName('Report Settings - Snapshot'); 219 | // Gets data from the snapshot sheet. 220 | const snapshotData = snapshotSheet.getRange( 221 | 1, 1, snapshotSheet.getLastRow(), snapshotSheet.getLastColumn() 222 | ).getValues(); 223 | const templateSlideIds = []; 224 | 225 | // Checks if more than just the headers exists for the snapshot data. 226 | if (snapshotData.length > 1) { 227 | // Loops through the snapshoot data starting at index 1 to skip the headers. 228 | for (let index = 1; index < snapshotData.length; index++) { 229 | const placeholders = snapshotData[0]; 230 | const values = snapshotData[index]; 231 | const templateSlideId = values[1] 232 | if (templateSlideIds.indexOf(templateSlideId) == -1) { 233 | templateSlideIds.push(templateSlideId); 234 | } 235 | // Finds the template slide in the presentation 236 | const templateSlide = presentation.getSlides().find( 237 | slide => slide.getNotesPage().getSpeakerNotesShape().getText() 238 | .asString().trim() == templateSlideId); 239 | // Makes a copy of the template slide 240 | if (templateSlide != undefined) { 241 | const templateSlideCopy = templateSlide.duplicate(); 242 | // Removes the text in the notes page in the copy. 243 | templateSlideCopy.getNotesPage().replaceAllText(templateSlideId, ''); 244 | 245 | const shapes = templateSlideCopy.getShapes(); 246 | // Loop through the placeholders and the values to replace the values. 247 | if (placeholders.length > 2) { 248 | for (let j = 2; j < placeholders.length; j++) { 249 | const placeholder = placeholders[j]; 250 | const value = values[j]; 251 | const selectedShape = shapes.find(shape => shape.getText() 252 | .find(placeholder).length > 0); 253 | replaceValues(placeholder, value, selectedShape, percentColorRanges); 254 | } 255 | } 256 | } 257 | } 258 | } 259 | // Removes original template slides. 260 | if (templateSlideIds.length > 0) { 261 | templateSlideIds.forEach(templateSlideId => { 262 | const templateSlide = presentation.getSlides().find( 263 | slide => slide.getNotesPage().getSpeakerNotesShape().getText() 264 | .asString().trim() == templateSlideId); 265 | templateSlide.remove(); 266 | }); 267 | } 268 | } 269 | 270 | /** 271 | * Replaces the placeholder with an unordered list of values. 272 | * @param {!string} templateValue 273 | * @param {!string} templatePlaceholder 274 | * @param {!Object} textRange 275 | * @param {!string} percentColorRanges 276 | */ 277 | function replaceWithList( 278 | templateValue, templatePlaceholder, 279 | textRange, percentColorRanges) { 280 | const listBullets = templateValue.split(';;'); 281 | if (listBullets.length > 0) { 282 | if (listBullets[listBullets.length - 1] == '') { 283 | listBullets.pop(); 284 | } 285 | for (let k = 0; k < listBullets.length; k++) { 286 | if (k == 0) { 287 | replaceWithTextOrNumber( 288 | textRange, templatePlaceholder, listBullets[k], percentColorRanges) 289 | .getListStyle().applyListPreset( 290 | SlidesApp.ListPreset.DISC_CIRCLE_SQUARE); 291 | } else { 292 | textRange.appendParagraph(listBullets[k]) 293 | .getRange().getListStyle().applyListPreset( 294 | SlidesApp.ListPreset.DISC_CIRCLE_SQUARE); 295 | } 296 | } 297 | } 298 | } 299 | 300 | /** 301 | * Converts the color range string from the sheet from a string to an object. 302 | * @param {!string} value The string value that needs to be reformatted. 303 | * @return {!Object} The color range object. 304 | */ 305 | function formatColorRangeObject(value) { 306 | return value.split(';').reduce((arr, range) => { 307 | arr.push({ 308 | start: range.split(',')[0].split('-')[0], 309 | end: range.split(',')[0].split('-')[1], 310 | hex: range.split(',')[1] 311 | }); 312 | return arr; 313 | }, []); 314 | } 315 | 316 | /** 317 | * Finds the template slides and replaces their content with values from the 318 | * sheet. 319 | * @param {!Object} presentation The Google Slides presentation. 320 | * @param {!Object} templateInfo The the template info object. 321 | * @param {!Object} percentColorRanges The color ranges for percent values 322 | * object. 323 | */ 324 | function replaceSingleSlidePlaceholders( 325 | presentation, templateInfo, percentColorRanges) { 326 | // Gets the template slide. 327 | const templateSlide = presentation.getSlides().find( 328 | slide => slide.getNotesPage().getSpeakerNotesShape().getText().asString() 329 | .trim() == templateInfo.templateSlideId); 330 | 331 | // Get the shapes in the template slide. 332 | const shapes = templateSlide.getShapes(); 333 | 334 | // Loop through the template pairs. 335 | for (let index = 0; index < templateInfo.templatePairs.length; index++) { 336 | const templatePair = templateInfo.templatePairs[index]; 337 | const templatePlaceholder = Object.keys(templatePair)[0]; 338 | const templateValue = templatePair[templatePlaceholder]; 339 | const selectedShape = shapes.find( 340 | element => element.getText().asString().trim() == templatePlaceholder); 341 | if (selectedShape != undefined && 342 | /logo|image|graphic/.test(templatePlaceholder)) { 343 | shapes.splice(shapes.indexOf(selectedShape), 1); 344 | } 345 | replaceValues(templatePlaceholder, templateValue, 346 | selectedShape, percentColorRanges); 347 | } 348 | } 349 | 350 | /** 351 | * Replaces the placeholder values in the slide with the values from the sheet. 352 | * @param {!string} placeholder The placeholder string to be replaced. 353 | * @param {!value} value The string value to replace the placeholder string. 354 | * @param {!Object} shape The slide shape with the placeholder string. 355 | * @param {!Object} percentColorRanges The color ranges for percent values 356 | * object. 357 | */ 358 | function replaceValues(placeholder, value, shape, percentColorRanges) { 359 | if (shape != undefined) { 360 | // Gets the text from the template slide shape. 361 | const textRange = shape.getText(); 362 | 363 | // Checks if the placeholder value is present in the template slide shape 364 | // text. 365 | if (textRange.find(placeholder).length > 0) { 366 | // Replaces the placeholder shape with an image with the same dimensions. 367 | if (/logo|image|graphic/.test(placeholder)) { 368 | replaceTemplateWithImage(shape, value); 369 | } else if (/list/.test(placeholder)) { 370 | replaceWithList(value, placeholder, textRange, percentColorRanges); 371 | } else { 372 | // Replaces the placholder text with the actual text. 373 | replaceWithTextOrNumber( 374 | textRange, placeholder, 375 | value, percentColorRanges); 376 | } 377 | } 378 | } 379 | } 380 | 381 | /** 382 | * Replaces the placeholder template string with an image. 383 | * @param {!Object} shape The shape to be replaced. 384 | * @param {!string} templateValue The URL for the image. 385 | */ 386 | function replaceTemplateWithImage(shape, templateValue) { 387 | const height = shape.getHeight(); 388 | const width = shape.getWidth(); 389 | if (templateValue.length > 0) { 390 | shape.replaceWithImage(templateValue).setHeight(height).setWidth(width); 391 | } 392 | } 393 | 394 | /** 395 | * Replaces the placeholder template string with the new value from the sheet. 396 | * This value from the sheet can be either a string or a number. If it is a 397 | * number, it is transformed into a string. 398 | * @param {!Object} textRange The text range that contains the placeholder 399 | * string. 400 | * @param {!string} templatePlaceholder The placeholder string to be replaced. 401 | * @param {!string} templateValue The value from the sheet that will replace the 402 | * placeholder string. 403 | * @param {!Object} percentColorRanges The color ranges for percent values 404 | * object. 405 | */ 406 | function replaceWithTextOrNumber( 407 | textRange, templatePlaceholder, templateValue, percentColorRanges) { 408 | // Sets the percent color. 409 | if (/percent/.test(templatePlaceholder)) { 410 | // Converts the decimal to a percent. 411 | const percentNumValue = Math.round(parseFloat(templateValue) * 100); 412 | // Converts the percent to a string with the percent sign at the end. 413 | const percentStringValue = percentNumValue.toString() + '%'; 414 | // Replaces the placeholder in the slide with the percent string value. 415 | textRange.replaceAllText(templatePlaceholder.trim(), percentStringValue); 416 | // Gets the range of the replaced value in order to change its color. 417 | const percentTextRanges = textRange.find(percentStringValue); 418 | // Loops through the percent color range values to set the color of the 419 | // replaced value correctly. 420 | percentTextRanges.forEach(percentTextRange => { 421 | if (percentColorRanges.length > 0) { 422 | percentColorRanges.forEach(range => { 423 | if (percentNumValue >= parseInt(range.start) && 424 | percentNumValue <= parseInt(range.end)) { 425 | percentTextRange.getTextStyle().setForegroundColor(range.hex); 426 | } 427 | }); 428 | } 429 | }); 430 | } else if (!isNaN(templateValue) && typeof templateValue != "boolean") { 431 | // Rounds any potential float to the nearest integer to display the value 432 | // more clearly on the slide. 433 | const numValue = Math.round(parseFloat(templateValue)); 434 | textRange.replaceAllText(templatePlaceholder.trim(), numValue); 435 | } else { 436 | // Replaces the placeholder with a non-numeric value. 437 | textRange.replaceAllText(templatePlaceholder.trim(), templateValue); 438 | } 439 | return textRange; 440 | } -------------------------------------------------------------------------------- /new_analytics/listGA4AccessBindings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Lists the users for all properties and their associated accounts. 19 | * @param {!Array} A double array of selected properties. 20 | * 21 | */ 22 | function listGA4AccessBindings(properties) { 23 | const accounts = []; 24 | const formattedAccessBindings = []; 25 | properties.forEach(property => { 26 | const accountName = 'accounts/' + property[1]; 27 | const propertyName = 'properties/' + property[3]; 28 | if (accounts.indexOf(accountName) == -1) { 29 | accounts.push(accountName); 30 | const accountAccessBindings = listGA4Entities( 31 | 'accountAccessBindings', accountName).accessBindings; 32 | if (accountAccessBindings) { 33 | accountAccessBindings.forEach(accessBinding => { 34 | formattedAccessBindings.push([ 35 | property[0], 36 | property[1], 37 | '', '', 38 | accessBinding.user, 39 | accessBinding.name, 40 | 'account', 41 | getPrimaryRoles(accessBinding.roles), 42 | getSecondaryRoles(accessBinding.roles) 43 | ]); 44 | }); 45 | } 46 | } 47 | 48 | if (property[3] != '') { 49 | const propertyAccessBindings = listGA4Entities( 50 | 'propertyAccessBindings', propertyName).accessBindings; 51 | if (propertyAccessBindings) { 52 | propertyAccessBindings.forEach(accessBinding => { 53 | formattedAccessBindings.push([ 54 | property[0], 55 | property[1], 56 | property[2], 57 | property[3], 58 | accessBinding.user, 59 | accessBinding.name, 60 | 'property', 61 | getPrimaryRoles(accessBinding.roles), 62 | getSecondaryRoles(accessBinding.roles) 63 | ]); 64 | }); 65 | } 66 | } 67 | }); 68 | return formattedAccessBindings; 69 | } 70 | 71 | /** 72 | * 73 | */ 74 | function getPrimaryRoles(roles) { 75 | const templateRoles = [ 76 | 'predefinedRoles/viewer', 77 | 'predefinedRoles/analyst', 78 | 'predefinedRoles/editor', 79 | 'predefinedRoles/admin' 80 | ]; 81 | if (roles.length > 0) { 82 | for (let i = 0; i < roles.length; i++) { 83 | if (templateRoles.indexOf(roles[i]) > 0) { 84 | return roles[i]; 85 | } 86 | } 87 | } 88 | } 89 | 90 | /** 91 | * 92 | */ 93 | function getSecondaryRoles(roles) { 94 | const templateRoles = [ 95 | 'predefinedRoles/no-cost-data', 96 | 'predefinedRoles/no-revenue-data' 97 | ]; 98 | let finalRoles = []; 99 | if (roles.length > 1) { 100 | for (let i = 0; i < roles.length; i++) { 101 | if (templateRoles.indexOf(roles[i]) > -1) { 102 | finalRoles.push(roles[i]); 103 | } 104 | } 105 | } 106 | return finalRoles.join(', '); 107 | } 108 | 109 | /** 110 | * Writes GA4 user link settings to a sheet. 111 | */ 112 | function writeGA4AccessBindingsToSheet() { 113 | const selectedProperties = getSelectedGa4Properties(); 114 | const accessBindings = listGA4AccessBindings(selectedProperties); 115 | clearSheetContent(sheetsMeta.ga4.accessBindings); 116 | writeToSheet(accessBindings, sheetsMeta.ga4.accessBindings.sheetName); 117 | } 118 | -------------------------------------------------------------------------------- /new_analytics/listGA4AccountSummaries.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * @param {!Array} summaries 18 | * @return {!Array} 19 | */ 20 | function getFlattenedGA4AccountSummaries(summaries) { 21 | const flatSummaries = []; 22 | if (summaries != undefined) { 23 | summaries.forEach(account => { 24 | const accountDisplayName = account.displayName; 25 | const accountId = account.account.split('/')[1]; 26 | if (account.propertySummaries != undefined) { 27 | account.propertySummaries.forEach(property => { 28 | const propertyDisplayName = property.displayName; 29 | const propertyId = property.property.split('/')[1]; 30 | flatSummaries.push([ 31 | accountDisplayName, 32 | accountId, 33 | propertyDisplayName, 34 | propertyId 35 | ]); 36 | }); 37 | } else { 38 | flatSummaries.push([accountDisplayName, accountId, '', '']); 39 | } 40 | }); 41 | } 42 | return flatSummaries; 43 | } 44 | 45 | /** 46 | * 47 | */ 48 | function writeGA4AccountSummariesToSheet() { 49 | let summaries = listGA4Entities('accountSummaries'); 50 | const flattenedSummaries = getFlattenedGA4AccountSummaries(summaries.accountSummaries); 51 | clearSheetContent(sheetsMeta.ga4.accountSummaries); 52 | writeToSheet(flattenedSummaries, sheetsMeta.ga4.accountSummaries.sheetName); 53 | } -------------------------------------------------------------------------------- /new_analytics/listGA4Audiences.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Retrieves the audiences for a given set of properties. 19 | * @param {!Array} properties A two dimensional array of 20 | * account and property names and ids. 21 | * @return {!Array} A two dimensional array where each 22 | * array contains metadata about the audiences for given 23 | * property. 24 | */ 25 | function listSelectedGA4Audiences(properties) { 26 | const formattedAudiences = []; 27 | properties.forEach(property => { 28 | const propertyName = 'properties/' + property[3]; 29 | const audiences = listGA4Entities( 30 | 'audiences', propertyName).audiences; 31 | if (audiences != undefined) { 32 | for (let i = 0; i < audiences.length; i++) { 33 | let filterClauses = audiences[i].filterClauses; 34 | let eventTrigger = {}; 35 | if (audiences[i].eventTrigger) { 36 | eventTrigger.eventName = audiences[i].eventTrigger.eventName; 37 | eventTrigger.logCondition = audiences[i].eventTrigger.logCondition; 38 | } 39 | if (filterClauses != undefined) { 40 | filterClauses = JSON.stringify(filterClauses); 41 | } 42 | formattedAudiences.push([ 43 | property[0], 44 | property[1], 45 | property[2], 46 | property[3], 47 | audiences[i].displayName, 48 | audiences[i].name, 49 | audiences[i].description, 50 | audiences[i].membershipDurationDays, 51 | audiences[i].adsPersonalizationEnabled, 52 | eventTrigger.eventName || '', 53 | eventTrigger.logCondition || '', 54 | audiences[i].exclusionDurationMode, 55 | filterClauses 56 | ]); 57 | } 58 | } 59 | }); 60 | return formattedAudiences; 61 | } 62 | 63 | /** 64 | * 65 | */ 66 | function writeGA4AudiencesToSheet() { 67 | const selectedProperties = getSelectedGa4Properties(); 68 | const audiences = listSelectedGA4Audiences(selectedProperties); 69 | clearSheetContent(sheetsMeta.ga4.audiences); 70 | writeToSheet(audiences, sheetsMeta.ga4.audiences.sheetName); 71 | resizeRowHeights(sheetsMeta.ga4.audiences.sheetName, 21); 72 | } -------------------------------------------------------------------------------- /new_analytics/listGA4BigQueryLinks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Retrieves the BigQuery links for a given set of properties. 19 | * @param {!Array} properties A two dimensional array of 20 | * account and property names and ids. 21 | * @return {!Array} A two dimensional array where each 22 | * array contains metadata about the BigQuery links for the given 23 | * set of properties. 24 | */ 25 | function listGA4BigQueryLinks(properties) { 26 | let sheetValuesArray = []; 27 | properties.forEach(property => { 28 | const propertyName = 'properties/' + property[3]; 29 | const links = listGA4Entities( 30 | 'bigqueryLinks', propertyName).bigqueryLinks; 31 | if (links != undefined) { 32 | links.forEach(link => { 33 | if (link.excludedEvents) { 34 | link.excludedEvents = link.excludedEvents.join(','); 35 | } 36 | if (link.exportStreams) { 37 | link.exportStreams = link.exportStreams.join(','); 38 | } 39 | sheetValuesArray.push([ 40 | property[0], 41 | property[1], 42 | property[2], 43 | property[3], 44 | link.project, 45 | link.name, 46 | link.includeAdvertisingId || '', 47 | link.dailyExportEnabled, 48 | link.streamingExportEnabled, 49 | link.freshDailyExportEnabled, 50 | link.excludedEvents || '', 51 | link.exportStreams || '', 52 | link.datasetLocation, 53 | link.createTime, 54 | ]); 55 | }); 56 | } 57 | }); 58 | return sheetValuesArray; 59 | } 60 | 61 | /** 62 | * Writes GA4 BigQuery information to a sheet. 63 | */ 64 | function writeGA4BigQueryLinksToSheet() { 65 | const selectedProperties = getSelectedGa4Properties(); 66 | const bigqueryLinks = listGA4BigQueryLinks(selectedProperties); 67 | clearSheetContent(sheetsMeta.ga4.bigqueryLinks); 68 | writeToSheet(bigqueryLinks, sheetsMeta.ga4.bigqueryLinks.sheetName); 69 | } -------------------------------------------------------------------------------- /new_analytics/listGA4CalculatedMetrics.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Retrieves the calculated metrics for a given set of properties. 19 | * @param {!Array} properties A two dimensional array of 20 | * account and property names and ids. 21 | * @return {!Array} A two dimensional array where each 22 | * array contains metadata about the calculated metrics for the given property. 23 | */ 24 | function listSelectedGA4CalculatedMetrics(properties) { 25 | const finalizedCalcMetrics = []; 26 | properties.forEach(property => { 27 | const propertyName = 'properties/' + property[3]; 28 | const calcMetrics = listGA4Entities( 29 | 'calculatedMetrics', propertyName).calculatedMetrics; 30 | if (calcMetrics != undefined) { 31 | for (let i = 0; i < calcMetrics.length; i++) { 32 | let rmt = ''; 33 | if (calcMetrics[i].restrictedMetricType) { 34 | rmt = calcMetrics[i].restrictedMetricType.join(', '); 35 | } 36 | finalizedCalcMetrics.push([ 37 | property[0], 38 | property[1], 39 | property[2], 40 | property[3], 41 | calcMetrics[i].displayName, 42 | calcMetrics[i].name, 43 | calcMetrics[i].description, 44 | calcMetrics[i].calculatedMetricId, 45 | calcMetrics[i].metricUnit, 46 | rmt, 47 | calcMetrics[i].formula, 48 | calcMetrics[i].invalidMetricReference 49 | ]); 50 | } 51 | } 52 | }); 53 | return finalizedCalcMetrics; 54 | } 55 | 56 | /** 57 | * Retrieves the calculated metrics for the selected properties and writes them 58 | * to the calculated metrics sheet. 59 | */ 60 | function writeGA4CalculatedMetricsToSheet() { 61 | const selectedProperties = getSelectedGa4Properties(); 62 | const calcMetrics = listSelectedGA4CalculatedMetrics(selectedProperties); 63 | clearSheetContent(sheetsMeta.ga4.calculatedMetrics); 64 | writeToSheet(calcMetrics, sheetsMeta.ga4.calculatedMetrics.sheetName); 65 | } -------------------------------------------------------------------------------- /new_analytics/listGA4ConversionEvents.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Retrieves the conversion events for a given set of properties. 19 | * @param {!Array} properties A two dimensional array of 20 | * account and property names and ids. 21 | * @return {!Array} A two dimensional array where each 22 | * array contains metadata about the conversion events for the given 23 | * set of properties. 24 | */ 25 | function listSelectedGA4ConversionEvents(properties) { 26 | const allConversionEvents = []; 27 | const request = generateGA4DataReportRequest( 28 | ['eventName', 'isConversionEvent'], 29 | ['conversions'], '7daysAgo', 'today', 30 | {name: 'isConversionEvent', matchType: 'EXACT', value: 'true'}); 31 | properties.forEach(property => { 32 | const propertyName = 'properties/' + property[3]; 33 | const conversionEvents = listGA4Entities( 34 | 'conversionEvents', propertyName).conversionEvents; 35 | if (conversionEvents) { 36 | let conversionsReport = null; 37 | try { 38 | conversionsReport = AnalyticsData.Properties.runReport( 39 | request, propertyName); 40 | } catch(e) { 41 | conversionsReport = 'No Access'; 42 | } 43 | for (let i = 0; i < conversionEvents.length; i++) { 44 | const currentConversionEvent = conversionEvents[i]; 45 | let conversionCount = null; 46 | if (conversionsReport != 'No Access') { 47 | conversionCount = getConversionCount( 48 | conversionsReport, currentConversionEvent); 49 | } else { 50 | conversionCount = conversionsReport; 51 | } 52 | allConversionEvents.push([ 53 | property[0], 54 | property[1], 55 | property[2], 56 | property[3], 57 | currentConversionEvent.eventName, 58 | currentConversionEvent.name, 59 | currentConversionEvent.createTime, 60 | currentConversionEvent.deletable, 61 | currentConversionEvent.custom, 62 | conversionCount 63 | ]); 64 | } 65 | } 66 | }); 67 | return allConversionEvents; 68 | } 69 | 70 | /** 71 | * Retrieves either the conversion count for a given event or zero. 72 | * @param {!Object} conversionsReport Analytics report object. 73 | * @param {!Object} conversionEvent 74 | * @return {number} conversionCount The number of conversions for a given event. 75 | */ 76 | function getConversionCount(conversionsReport, conversionEvent) { 77 | let conversionCount = 0; 78 | if (conversionsReport.rows) { 79 | conversionRow = conversionsReport.rows.find( 80 | row => row.dimensionValues[0].value == conversionEvent.eventName); 81 | if (conversionRow) { 82 | conversionCount = conversionRow.metricValues[0].value || 0; 83 | } 84 | } 85 | return conversionCount; 86 | } 87 | 88 | /** 89 | * 90 | */ 91 | function writeGA4ConversionEventsToSheet() { 92 | const selectedProperties = getSelectedGa4Properties(); 93 | const conversionEvents = listSelectedGA4ConversionEvents(selectedProperties); 94 | clearSheetContent(sheetsMeta.ga4.conversionEvents); 95 | writeToSheet(conversionEvents, sheetsMeta.ga4.conversionEvents.sheetName); 96 | } -------------------------------------------------------------------------------- /new_analytics/listGA4CustomDimensions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Retrieves the custom dimensions for a given set of properties. 19 | * @param {!Array} properties A two dimensional array of 20 | * account and property names and ids. 21 | * @return {!Array} A two dimensional array where each 22 | * array contains metadata about the custom dimensions for given 23 | * property. 24 | */ 25 | function listSelectedGA4CustomDimensions(properties) { 26 | const finalizedCds = []; 27 | properties.forEach(property => { 28 | const propertyName = 'properties/' + property[3]; 29 | const cds = listGA4Entities( 30 | 'customDimensions', propertyName).customDimensions; 31 | if (cds != undefined) { 32 | for (let i = 0; i < cds.length; i++) { 33 | const currentCd = cds[i]; 34 | finalizedCds.push([ 35 | property[0], 36 | property[1], 37 | property[2], 38 | property[3], 39 | currentCd.displayName, 40 | currentCd.name, 41 | currentCd.parameterName, 42 | currentCd.scope, 43 | currentCd.disallowAdsPersonalization, 44 | currentCd.description 45 | ]); 46 | } 47 | } 48 | }); 49 | return finalizedCds; 50 | } 51 | 52 | /** 53 | * 54 | */ 55 | function writeGA4CustomDimensionsToSheet() { 56 | const selectedProperties = getSelectedGa4Properties(); 57 | const cds = listSelectedGA4CustomDimensions(selectedProperties); 58 | clearSheetContent(sheetsMeta.ga4.customDimensions); 59 | writeToSheet(cds, sheetsMeta.ga4.customDimensions.sheetName); 60 | } -------------------------------------------------------------------------------- /new_analytics/listGA4CustomMetrics.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Retrieves the custom metrics for a given set of properties. 19 | * @param {!Array} properties A two dimensional array of 20 | * account and property names and ids. 21 | * @return {!Array} A two dimensional array where each 22 | * array contains metadata about the custom metrics for the given 23 | * set of properties. 24 | */ 25 | function listSelectedGA4CustomMetrics(properties) { 26 | const finalizedCms = []; 27 | properties.forEach(property => { 28 | const propertyName = 'properties/' + property[3]; 29 | const cms = listGA4Entities( 30 | 'customMetrics', propertyName).customMetrics; 31 | if (cms != undefined) { 32 | for (let i = 0; i < cms.length; i++) { 33 | let rmt = ''; 34 | if (cms[i].restrictedMetricType) { 35 | rmt = cms[i].restrictedMetricType.join(', ') 36 | } 37 | finalizedCms.push([ 38 | property[0], 39 | property[1], 40 | property[2], 41 | property[3], 42 | cms[i].displayName, 43 | cms[i].name, 44 | cms[i].parameterName, 45 | cms[i].scope, 46 | cms[i].measurementUnit, 47 | cms[i].description, 48 | rmt 49 | ]); 50 | } 51 | } 52 | }); 53 | return finalizedCms; 54 | } 55 | 56 | /** 57 | * 58 | */ 59 | function writeGA4CustomMetricsToSheet() { 60 | const selectedProperties = getSelectedGa4Properties(); 61 | const cms = listSelectedGA4CustomMetrics(selectedProperties); 62 | clearSheetContent(sheetsMeta.ga4.customMetrics); 63 | writeToSheet(cms, sheetsMeta.ga4.customMetrics.sheetName); 64 | } -------------------------------------------------------------------------------- /new_analytics/listGA4DV360Links.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Retrieves the Google DV360 links for a given set of properties. 19 | * @param {!Array} properties A two dimensional array of 20 | * account and property names and ids. 21 | * @return {!Array} A two dimensional array where each 22 | * array contains metadata about the Google DV360 Links for the given 23 | * set of properties. 24 | */ 25 | function listSelectedDV360Links(properties) { 26 | const links = []; 27 | properties.forEach(property => { 28 | const propertyName = 'properties/' + property[3]; 29 | const allDV360Links = listGA4Entities( 30 | 'displayVideo360AdvertiserLinks', propertyName) 31 | .displayVideo360AdvertiserLinks; 32 | if (allDV360Links != undefined) { 33 | for (let i = 0; i < allDV360Links.length; i++) { 34 | links.push([ 35 | property[0], 36 | property[1], 37 | property[2], 38 | property[3], 39 | allDV360Links[i].advertiserId, 40 | allDV360Links[i].name, 41 | allDV360Links[i].advertiserDisplayName, 42 | allDV360Links[i].adsPersonalizationEnabled, 43 | allDV360Links[i].campaignDataSharingEnabled, 44 | allDV360Links[i].costDataSharingEnabled 45 | ]); 46 | } 47 | } 48 | }); 49 | return links; 50 | } 51 | 52 | /** 53 | * 54 | */ 55 | function writeGA4DV360LinksToSheet() { 56 | const selectedProperties = getSelectedGa4Properties(); 57 | const links = listSelectedDV360Links(selectedProperties); 58 | clearSheetContent(sheetsMeta.ga4.displayVideo360AdvertiserLinks); 59 | writeToSheet(links, sheetsMeta.ga4.displayVideo360AdvertiserLinks.sheetName); 60 | } -------------------------------------------------------------------------------- /new_analytics/listGA4ExpandedDatasets.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Retrieves the expanded data sets for a given set of properties. 19 | * @param {!Array} properties A two dimensional array of 20 | * account and property names and ids. 21 | * @return {!Array} A two dimensional array where each 22 | * array contains metadata about the expanded datasets for the given 23 | * set of properties. 24 | */ 25 | function listGA4ExpandedDataSets(properties) { 26 | let sheetValuesArray = []; 27 | properties.forEach(property => { 28 | const propertyName = 'properties/' + property[3]; 29 | const dataSets = listGA4Entities( 30 | 'expandedDataSets', propertyName).expandedDataSets; 31 | if (dataSets != undefined) { 32 | dataSets.forEach(dataSet => { 33 | let filterExpression = ''; 34 | if (dataSet.dimensionFilterExpression) { 35 | filterExpression = dataSet.dimensionFilterExpression.toString(); 36 | } 37 | sheetValuesArray.push([ 38 | property[0], 39 | property[1], 40 | property[2], 41 | property[3], 42 | dataSet.displayName, 43 | dataSet.name, 44 | dataSet.description, 45 | dataSet.dimensionNames.join(', '), 46 | dataSet.metricNames.join(', '), 47 | filterExpression, 48 | dataSet.dataCollectionStartTime 49 | ]); 50 | }); 51 | } 52 | }); 53 | return sheetValuesArray; 54 | } 55 | 56 | /** 57 | * Writes GA4 expanded dataSet information to a sheet. 58 | */ 59 | function writeGA4ExpandedDataSetsToSheet() { 60 | const selectedProperties = getSelectedGa4Properties(); 61 | const expandedDataSets = listGA4ExpandedDataSets(selectedProperties); 62 | clearSheetContent(sheetsMeta.ga4.expandedDataSets); 63 | writeToSheet(expandedDataSets, sheetsMeta.ga4.expandedDataSets.sheetName); 64 | } -------------------------------------------------------------------------------- /new_analytics/listGA4FirebaseLinks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Retrieves the Firebase links for a given set of properties. 19 | * @param {!Array} properties A two dimensional array of 20 | * account and property names and ids. 21 | * @return {!Array} A two dimensional array where each 22 | * array contains metadata about the Firebase links for the given 23 | * set of properties. 24 | */ 25 | function listSelectedGA4FirebaseLinks(properties) { 26 | const allFirebaseLinks = []; 27 | properties.forEach(property => { 28 | const propertyName = 'properties/' + property[3]; 29 | const firebaseLinks = listGA4Entities( 30 | 'firebaseLinks', propertyName).firebaseLinks; 31 | if (firebaseLinks != undefined) { 32 | for (let i = 0; i < firebaseLinks.length; i++) { 33 | allFirebaseLinks.push([ 34 | property[0], 35 | property[1], 36 | property[2], 37 | property[3], 38 | firebaseLinks[i].project, 39 | firebaseLinks[i].name, 40 | firebaseLinks[i].createTime 41 | ]); 42 | } 43 | } 44 | }); 45 | return allFirebaseLinks; 46 | } 47 | 48 | /** 49 | * 50 | */ 51 | function writeGA4FirebaseLinksToSheet() { 52 | const selectedProperties = getSelectedGa4Properties(); 53 | const firebaseLinks = listSelectedGA4FirebaseLinks(selectedProperties); 54 | clearSheetContent(sheetsMeta.ga4.firebaseLinks); 55 | writeToSheet(firebaseLinks, sheetsMeta.ga4.firebaseLinks.sheetName); 56 | } -------------------------------------------------------------------------------- /new_analytics/listGA4GoogleAdsLinks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Retrieves the Google Ads links for a given set of properties. 19 | * @param {!Array} properties A two dimensional array of 20 | * account and property names and ids. 21 | * @return {!Array} A two dimensional array where each 22 | * array contains metadata about the Google Ads Links for the given 23 | * set of properties. 24 | */ 25 | function listSelectedGA4AdsLinks(properties) { 26 | const allAdsLinks = []; 27 | properties.forEach(property => { 28 | const propertyName = 'properties/' + property[3]; 29 | const adsLinks = listGA4Entities( 30 | 'googleAdsLinks', propertyName).googleAdsLinks; 31 | if (adsLinks != undefined) { 32 | for (let i = 0; i < adsLinks.length; i++) { 33 | allAdsLinks.push([ 34 | property[0], 35 | property[1], 36 | property[2], 37 | property[3], 38 | adsLinks[i].customerId, 39 | adsLinks[i].name, 40 | adsLinks[i].canManageClients, 41 | adsLinks[i].adsPersonalizationEnabled, 42 | adsLinks[i].emailAddress, 43 | adsLinks[i].createTime, 44 | adsLinks[i].updateTime 45 | ]); 46 | } 47 | } 48 | }); 49 | return allAdsLinks; 50 | } 51 | 52 | /** 53 | * 54 | */ 55 | function writeGA4AdsLinksToSheet() { 56 | const selectedProperties = getSelectedGa4Properties(); 57 | const adsLinks = listSelectedGA4AdsLinks(selectedProperties); 58 | clearSheetContent(sheetsMeta.ga4.googleAdsLinks); 59 | writeToSheet(adsLinks, sheetsMeta.ga4.googleAdsLinks.sheetName); 60 | } -------------------------------------------------------------------------------- /new_analytics/listGA4KeyEvents.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Retrieves the key events for a given set of properties. 19 | * @param {!Array} properties A two dimensional array of 20 | * account and property names and ids. 21 | * @return {!Array} A two dimensional array where each 22 | * array contains metadata about the key events for the given 23 | * set of properties. 24 | */ 25 | function listSelectedGA4KeyEvents(properties) { 26 | const allKeyEvents = []; 27 | const request = generateGA4DataReportRequest( 28 | ['eventName', 'isKeyEvent'], 29 | ['keyEvents'], '7daysAgo', 'today', 30 | {name: 'isKeyEvent', matchType: 'EXACT', value: 'true'}); 31 | properties.forEach(property => { 32 | const propertyId = property[3]; 33 | const propertyName = 'properties/' + propertyId; 34 | const keyEvents = listGA4Entities( 35 | 'keyEvents', propertyName).keyEvents; 36 | if (keyEvents) { 37 | let keyEventsReportResponse = {}; 38 | let hasAccess = false; 39 | const noAccessText = 'No Access'; 40 | try { 41 | keyEventsReportResponse = AnalyticsData.Properties.runReport( 42 | request, propertyName); 43 | hasAccess = true; 44 | } catch(e) { 45 | } 46 | for (let i = 0; i < keyEvents.length; i++) { 47 | const currentKeyEvent = keyEvents[i]; 48 | let keyEventCount = 0; 49 | if (hasAccess) { 50 | keyEventCount = getkeyEventCount( 51 | keyEventsReportResponse, currentKeyEvent); 52 | } 53 | allKeyEvents.push([ 54 | ...property.slice(0, 4), 55 | currentKeyEvent.eventName, 56 | currentKeyEvent.name, 57 | currentKeyEvent.createTime, 58 | currentKeyEvent.deletable, 59 | currentKeyEvent.custom, 60 | hasAccess ? keyEventCount : noAccessText, 61 | currentKeyEvent.countingMethod, 62 | currentKeyEvent.defaultValue 63 | ]); 64 | } 65 | } 66 | }); 67 | return allKeyEvents; 68 | } 69 | 70 | /** 71 | * Retrieves either the key event count for a given event or zero. 72 | * @param {!Object} keyEventsReport Analytics report object. 73 | * @param {!Object} keyEvent 74 | * @return {number} keyEventCount The number of key events for a given event. 75 | */ 76 | function getkeyEventCount(keyEventsReport, keyEvent) { 77 | let keyEventCount = 0; 78 | if (keyEventsReport.rows) { 79 | keyEventRow = keyEventsReport.rows.find( 80 | row => row.dimensionValues[0].value == keyEvent.eventName); 81 | if (keyEventRow) { 82 | keyEventCount = keyEventRow.metricValues[0].value || 0; 83 | } 84 | } 85 | return keyEventCount; 86 | } 87 | 88 | /** 89 | * 90 | */ 91 | function writeGA4KeyEventsToSheet() { 92 | const selectedProperties = getSelectedGa4Properties(); 93 | const keyEvents = listSelectedGA4KeyEvents(selectedProperties); 94 | clearSheetContent(sheetsMeta.ga4.keyEvents); 95 | writeToSheet(keyEvents, sheetsMeta.ga4.keyEvents.sheetName); 96 | } -------------------------------------------------------------------------------- /new_analytics/listGA4Properties.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Retrieves the property details for a given set of accounts. 19 | * @param {!Array} properties A two dimensional array of 20 | * account and property names and ids. 21 | * @return {!Array} A two dimensional array where each 22 | * inner array contains metadata for a given property. 23 | */ 24 | function listSelectedGA4Properties(selectedProperties) { 25 | let data = []; 26 | const request = generateGA4DataReportRequest([], ['eventCount'], '7daysAgo', 'today', {}); 27 | const alreadyListedAccounts = []; 28 | let subpropertyFilters = []; 29 | selectedProperties.forEach(property => { 30 | if (alreadyListedAccounts.indexOf(property[1]) == -1) { 31 | alreadyListedAccounts.push(property[1]); 32 | const parent = {filter: 'ancestor:accounts/' + property[1], pageSize: 200}; 33 | const properties = listGA4Entities('properties', parent).properties; 34 | if (properties) { 35 | data = data.concat(properties.reduce((arr, prop) => { 36 | if (selectedProperties.filter(property => property[3] == prop.name.split('/')[1]).length > 0) { 37 | const attributionSettings = getGA4Resource( 38 | 'attributionSettings', prop.name + '/attributionSettings'); 39 | const dataRetentionSettings = getGA4Resource( 40 | 'dataRetentionSettings', prop.name + '/dataRetentionSettings'); 41 | const googleSignalsSettings = getGA4Resource( 42 | 'googleSignalsSettings', prop.name + '/googleSignalsSettings'); 43 | let eventCount = 0; 44 | let eventRows = null; 45 | let sourceProperties = ''; 46 | let subpropertyFilter = ''; 47 | let subpropertyParent = ''; 48 | try { 49 | eventRows = AnalyticsData.Properties.runReport( 50 | request, prop.name).rows; 51 | } catch(e) { 52 | eventCount = 'No Access'; 53 | } 54 | if (eventRows) { 55 | eventCount = eventRows[0].metricValues[0].value; 56 | } 57 | if (prop.propertyType == 'PROPERTY_TYPE_ROLLUP') { 58 | sourceProperties = listGA4Entities( 59 | 'rollupPropertySourceLinks', prop.name).rollupPropertySourceLinks; 60 | sourceProperties = sourceProperties.reduce((linkArray, link) => { 61 | linkArray.push(link.sourceProperty); 62 | return linkArray; 63 | }, []).join(','); 64 | } else if (prop.propertyType == 'PROPERTY_TYPE_SUBPROPERTY') { 65 | if (prop.parent != subpropertyParent) { 66 | subpropertyParent = prop.parent; 67 | subpropertyFilters = listGA4Entities( 68 | 'subpropertyEventFilters', subpropertyParent).subpropertyEventFilters || []; 69 | } 70 | subpropertyFilter = subpropertyFilters.find( 71 | filter => prop.name == filter.applyToProperty); 72 | if (subpropertyFilter != undefined && subpropertyFilter != '') { 73 | subpropertyFilter = JSON.stringify(subpropertyFilter); 74 | } 75 | } 76 | const subArray = [ 77 | property[0], 78 | property[1], 79 | prop.displayName, 80 | prop.name.split('/')[1], 81 | prop.propertyType, 82 | prop.parent, 83 | sourceProperties, 84 | subpropertyFilter, 85 | prop.createTime, 86 | prop.updateTime, 87 | prop.industryCategory, 88 | prop.timeZone, 89 | prop.currencyCode, 90 | prop.serviceLevel, 91 | eventCount, 92 | googleSignalsSettings.state, 93 | dataRetentionSettings.eventDataRetention, 94 | dataRetentionSettings.resetUserDataOnNewActivity || false, 95 | attributionSettings.acquisitionConversionEventLookbackWindow, 96 | attributionSettings.otherConversionEventLookbackWindow, 97 | attributionSettings.reportingAttributionModel 98 | ] 99 | arr.push(subArray); 100 | return arr; 101 | } 102 | return arr; 103 | }, [])); 104 | } 105 | } 106 | }); 107 | return data; 108 | } 109 | 110 | /** 111 | * 112 | */ 113 | function writeGA4PropertyDetailsToSheet() { 114 | const selectedProperties = getSelectedGa4Properties(); 115 | const properties = listSelectedGA4Properties(selectedProperties); 116 | clearSheetContent(sheetsMeta.ga4.properties); 117 | writeToSheet(properties, sheetsMeta.ga4.properties.sheetName); 118 | } -------------------------------------------------------------------------------- /new_analytics/listGA4SA360Links.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Retrieves the SA360 links for a given set of properties. 19 | * @param {!Array} properties A two dimensional array of 20 | * account and property names and ids. 21 | * @return {!Array} A two dimensional array where each 22 | * array contains metadata about the SA360 links for the given 23 | * set of properties. 24 | */ 25 | function listGA4SA360Links(properties) { 26 | let sheetValuesArray = []; 27 | properties.forEach(property => { 28 | const propertyName = 'properties/' + property[3]; 29 | const links = listGA4Entities( 30 | 'searchAds360Links', propertyName).searchAds360Links; 31 | if (links != undefined) { 32 | links.forEach(link => { 33 | sheetValuesArray.push([ 34 | property[0], 35 | property[1], 36 | property[2], 37 | property[3], 38 | link.advertiserId, 39 | link.name, 40 | link.advertiserDisplayName, 41 | link.campaignDataSharingEnabled, 42 | link.costDataSharingEnabled, 43 | link.siteStatsSharingEnabled 44 | ]); 45 | }); 46 | } 47 | }); 48 | return sheetValuesArray; 49 | } 50 | 51 | /** 52 | * Writes GA4 SA360 link information to a sheet. 53 | */ 54 | function writeGA4SA360LinksToSheet() { 55 | const selectedProperties = getSelectedGa4Properties(); 56 | const sa360Links = listGA4SA360Links(selectedProperties); 57 | clearSheetContent(sheetsMeta.ga4.sa360Links); 58 | writeToSheet(sa360Links, sheetsMeta.ga4.sa360Links.sheetName); 59 | } -------------------------------------------------------------------------------- /new_analytics/listGA4Streams.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * 19 | */ 20 | function listSelectedGA4Streams(properties) { 21 | const dataStreams = []; 22 | properties.forEach(property => { 23 | const propertyName = 'properties/' + property[3]; 24 | const dataStreamsResponse = listGA4Entities( 25 | 'streams', propertyName).dataStreams; 26 | if (dataStreamsResponse != undefined && dataStreamsResponse.length > 0) { 27 | dataStreamsResponse.forEach(stream => { 28 | let enhancedMeasurementSettings = {}; 29 | if (stream.webStreamData) { 30 | enhancedMeasurementSettings = AnalyticsAdmin 31 | .Properties.DataStreams.getEnhancedMeasurementSettings( 32 | `${stream.name}/enhancedMeasurementSettings`); 33 | } 34 | const tempArray = []; 35 | tempArray.push( 36 | property[0], 37 | property[1], 38 | property[2], 39 | property[3], 40 | stream.displayName, 41 | stream.name, 42 | stream.type, 43 | '', '', '', '', 44 | stream.createTime, 45 | stream.updateTime, 46 | '', '', '', '', '', '', '', '', '' ,'', ''); 47 | if (stream.webStreamData != undefined) { 48 | tempArray[7] = stream.webStreamData.measurementId || ''; 49 | tempArray[10] = stream.webStreamData.firebaseAppId || ''; 50 | tempArray[13] = stream.webStreamData.defaultUri || ''; 51 | tempArray[14] = enhancedMeasurementSettings.streamEnabled || false, 52 | tempArray[15] = enhancedMeasurementSettings.scrollsEnabled || false, 53 | tempArray[16] = enhancedMeasurementSettings.outboundClicksEnabled || false, 54 | tempArray[17] = enhancedMeasurementSettings.siteSearchEnabled || false, 55 | tempArray[18] = enhancedMeasurementSettings.videoEngagementEnabled || false, 56 | tempArray[19] = enhancedMeasurementSettings.fileDownloadsEnabled || false, 57 | tempArray[20] = enhancedMeasurementSettings.pageChangesEnabled || false, 58 | tempArray[21] = enhancedMeasurementSettings.formInteractionsEnabled || false, 59 | tempArray[22] = enhancedMeasurementSettings.searchQueryParameter, 60 | tempArray[23] = enhancedMeasurementSettings.uriQueryParameter 61 | } else if (stream.androidAppStreamData != undefined) { 62 | tempArray[8] = stream.androidAppStreamData.packageName || ''; 63 | tempArray[10] = stream.androidAppStreamData.firebaseAppId || ''; 64 | } else if (stream.iosAppStreamData != undefined) { 65 | tempArray[9] = stream.iosAppStreamData.bundleId || ''; 66 | tempArray[10] = stream.iosAppStreamData.firebaseAppId || ''; 67 | } 68 | dataStreams.push(tempArray); 69 | }); 70 | } 71 | }); 72 | return dataStreams; 73 | } 74 | 75 | /** 76 | * Writes data streams to a sheet. 77 | */ 78 | function writeGA4StreamsToSheet() { 79 | const selectedProperties = getSelectedGa4Properties(); 80 | const streams = listSelectedGA4Streams(selectedProperties); 81 | clearSheetContent(sheetsMeta.ga4.streams); 82 | writeToSheet(streams, sheetsMeta.ga4.streams.sheetName); 83 | } -------------------------------------------------------------------------------- /new_analytics/listGA4UserLinks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Lists the users for all properties and their associated accounts. 19 | * @param {!Array} A double array of selected properties. 20 | * 21 | */ 22 | function listGA4UserLinks(properties) { 23 | const accounts = []; 24 | const formattedUserLinks = []; 25 | properties.forEach(property => { 26 | const accountName = 'accounts/' + property[1]; 27 | const propertyName = 'properties/' + property[3]; 28 | if (accounts.indexOf(accountName) == -1) { 29 | accounts.push(accountName); 30 | const accountUserLinks = listGA4Entities('accountUserLinks', accountName).userLinks; 31 | if (accountUserLinks) { 32 | accountUserLinks.forEach(userLink => { 33 | formattedUserLinks.push([ 34 | property[0], 35 | property[1], 36 | '', '', 37 | userLink.emailAddress, 38 | userLink.name, 39 | 'account', 40 | userLink.directRoles.join(', ') 41 | ]); 42 | }); 43 | } 44 | } 45 | const propertyUserLinks = listGA4Entities('propertyUserLinks', propertyName).userLinks; 46 | if (propertyUserLinks) { 47 | propertyUserLinks.forEach(userLink => { 48 | formattedUserLinks.push([ 49 | property[0], 50 | property[1], 51 | property[2], 52 | property[3], 53 | userLink.emailAddress, 54 | userLink.name, 55 | 'property', 56 | userLink.directRoles.join(', ') 57 | ]); 58 | }); 59 | } 60 | }); 61 | return formattedUserLinks; 62 | } 63 | 64 | /** 65 | * Writes GA4 user link settings to a sheet. 66 | */ 67 | function writeGA4UserLinksToSheet() { 68 | const selectedProperties = getSelectedGa4Properties(); 69 | const userLinks = listGA4UserLinks(selectedProperties); 70 | clearSheetContent(sheetsMeta.ga4.userLinks); 71 | writeToSheet(userLinks, sheetsMeta.ga4.userLinks.sheetName); 72 | } -------------------------------------------------------------------------------- /new_analytics/listRollupPropertySourceLinks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Retrieves the rollup property source links for a given set of properties. 19 | * @param {!Array} properties A two dimensional array of 20 | * account and property names and ids. 21 | * @return {!Array} A two dimensional array where each 22 | * array contains metadata about the rollup property source links. 23 | */ 24 | function listGA4RollupPropertySourceLinks(properties) { 25 | let sheetValuesArray = []; 26 | properties.forEach(property => { 27 | const propertyName = 'properties/' + property[3]; 28 | const links = listGA4Entities( 29 | 'rollupPropertySourceLinks', propertyName).rollupPropertySourceLinks; 30 | if (links != undefined) { 31 | links.forEach(link => { 32 | sheetValuesArray.push([ 33 | property[0], 34 | property[1], 35 | property[2], 36 | property[3], 37 | link.sourceProperty, 38 | link.name 39 | ]); 40 | }); 41 | } 42 | }); 43 | return sheetValuesArray; 44 | } 45 | 46 | /** 47 | * Writes GA4 rollup property source links to a sheet. 48 | */ 49 | function writeGA4RollupPropertySourceLinksToSheet() { 50 | const selectedProperties = getSelectedGa4Properties(); 51 | const rollupPropertySourceLinks = listGA4RollupPropertySourceLinks(selectedProperties); 52 | clearSheetContent(sheetsMeta.ga4.rollupPropertySourceLinks); 53 | writeToSheet(rollupPropertySourceLinks, sheetsMeta.ga4.rollupPropertySourceLinks.sheetName); 54 | } -------------------------------------------------------------------------------- /new_analytics/listSubpropertyEventFilters.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Retrieves the subproperty event filters for a given set of properties. 19 | * @param {!Array} properties A two dimensional array of 20 | * account and property names and ids. 21 | * @return {!Array} A two dimensional array where each 22 | * array contains metadata about the subproperty event filters. 23 | */ 24 | function listGA4SubpropertyEventFilters(properties) { 25 | let sheetValuesArray = []; 26 | properties.forEach(property => { 27 | const propertyName = 'properties/' + property[3]; 28 | const eventFilters = listGA4Entities( 29 | 'subpropertyEventFilters', propertyName).subpropertyEventFilters; 30 | if (eventFilters != undefined) { 31 | eventFilters.forEach(eventFilter => { 32 | sheetValuesArray.push([ 33 | property[0], 34 | property[1], 35 | property[2], 36 | property[3], 37 | eventFilter.applyToProperty, 38 | eventFilter.name, 39 | JSON.stringify(eventFilter.filterClauses) 40 | ]); 41 | }); 42 | } 43 | }); 44 | return sheetValuesArray; 45 | } 46 | 47 | /** 48 | * Writes GA4 subproperty event filters to a sheet. 49 | */ 50 | function writeGA4SubpropertyEventFiltersToSheet() { 51 | const selectedProperties = getSelectedGa4Properties(); 52 | const subpropertyEventFilters = listGA4SubpropertyEventFilters(selectedProperties); 53 | clearSheetContent(sheetsMeta.ga4.subpropertyEventFilters); 54 | writeToSheet(subpropertyEventFilters, sheetsMeta.ga4.subpropertyEventFilters.sheetName); 55 | } -------------------------------------------------------------------------------- /new_analytics/measurementProtocolSecrets.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Retrieves the measurement protocol secrets for a given set of data streams. 19 | * @param {!Array} streams A two dimensional array of 20 | * account and property names and ids. 21 | * @return {!Array} A two dimensional array where each 22 | * array contains metadata about the channel groups for the given 23 | * set of properties. 24 | */ 25 | function listGA4MeasurementProtocolSecrets(streams) { 26 | let sheetValuesArray = []; 27 | streams.forEach(stream => { 28 | const streamName = `properties/${stream[3]}/dataStreams/${stream[5]}`; 29 | const secrets = listGA4Entities( 30 | 'measurementProtocolSecrets', streamName).measurementProtocolSecrets; 31 | if (secrets) { 32 | secrets.forEach(secret => { 33 | sheetValuesArray.push([ 34 | stream[0], 35 | stream[1], 36 | stream[2], 37 | stream[3], 38 | stream[4], 39 | stream[5], 40 | secret.displayName, 41 | secret.name, 42 | secret.secretValue 43 | ]); 44 | }); 45 | } 46 | }); 47 | return sheetValuesArray; 48 | } 49 | 50 | /** 51 | * Writes GA4 measurement protocol secrets to a sheet. 52 | */ 53 | function writeGA4MeasurementProtocolSecretsToSheet() { 54 | const selectedStreams = getSelectedGa4DataStreams(); 55 | const mps = listGA4MeasurementProtocolSecrets(selectedStreams); 56 | clearSheetContent(sheetsMeta.ga4.measurementProtocolSecrets); 57 | writeToSheet(mps, sheetsMeta.ga4.measurementProtocolSecrets.sheetName); 58 | } -------------------------------------------------------------------------------- /new_analytics/userAccessReport.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Gets the user report settings from the "User Access Report Settings" sheet. 19 | * @return {!Array>} A double array of settings 20 | * data. 21 | */ 22 | function getUserAccessReportRequestSettingsFromSheet() { 23 | const sheetName = sheetsMeta.ga4.userAccessReportSettings.sheetName; 24 | const values = ss.getSheetByName(sheetName).getRange(1, 2, 12, 1).getValues(); 25 | return new UserAccessReportRequestSettings(values.flat()); 26 | } 27 | 28 | /** 29 | * Send user access report request. 30 | * @param {!UserAccessReportRequest} requestSettings 31 | * @param {string} entityString 32 | * @return {!Object} A user access report response object. 33 | */ 34 | function sendUserAccessReportRequest(requestSettings, entityString) { 35 | delete requestSettings.accountLevelReport; 36 | if (/accounts/.test(entityString)) { 37 | return AnalyticsAdmin.Accounts.runAccessReport( 38 | requestSettings, entityString); 39 | } else { 40 | return AnalyticsAdmin.Properties.runAccessReport( 41 | requestSettings, entityString); 42 | } 43 | } 44 | 45 | /** 46 | * Requests account or property user access reports for the for the specified 47 | * entities and returns a double array where each inner array contains a user 48 | * access record. 49 | * @param {!Array} entities An array of entity objects. 50 | * @param {!UserAccessReportRequestSettings} requestSettings The request 51 | * settings from the user access report settings sheet. 52 | * @param {!Array>} A double array representing the user access 53 | * records. 54 | */ 55 | function getAllUserAccessRecords(entities, requestSettings) { 56 | const records = []; 57 | // Loop through the selected entities. 58 | for (const currentEntity of entities) { 59 | let response = {}; 60 | // Send account level user access report request if the settings indicate 61 | // that the request is to be made at the account level. 62 | if (requestSettings.accountLevelReport) { 63 | response = sendUserAccessReportRequest( 64 | requestSettings, `${ENTITY_PREFIX.ACCOUNTS}${currentEntity.accountId}`); 65 | } else { 66 | response = sendUserAccessReportRequest( 67 | requestSettings, 68 | `${ENTITY_PREFIX.PROPERTIES}${currentEntity.propertyId}`); 69 | } 70 | // If the there are rows in the response, then loop through them and push 71 | // the dimension and metric values of each row to the records array while 72 | // adding the current entity values. 73 | if (response.rowCount > 0) { 74 | for (const row of response.rows) { 75 | records.push([ 76 | currentEntity.accountName, 77 | currentEntity.accountId, 78 | currentEntity.propertyName, 79 | currentEntity.propertyId, 80 | ...row.dimensionValues.map(dimension => dimension.value), 81 | ...row.metricValues.map(metric => metric.value), 82 | ]); 83 | } 84 | } 85 | } 86 | return records; 87 | } 88 | 89 | /** 90 | * Generates a array of selected entities based on the data in the account 91 | * summaries sheet. 92 | * @param {!Array>} sheetRows A double array of data from 93 | * the account summaries array. 94 | * @param {!UserAccessReportRequestSettings} requestSettings The request 95 | * settings from the user access report settings sheet. 96 | * @return {!Array} An array of entity objects. 97 | */ 98 | function createUserAccessReportSelectedEntitiesArray( 99 | sheetRows, requestSettings) { 100 | const previouslyExistingIds = []; 101 | const entities = []; 102 | // Loop through each row of the sheet data. 103 | for (const row of sheetRows) { 104 | const accountId = row[1]; 105 | // If the access report will be run at the account level, then only set 106 | // the account name and ID for a selected entity once. If the access 107 | // report will be run at the property level, then create an entity that 108 | // includes the account name, account ID, property name, and property ID. 109 | if (requestSettings.accountLevelReport) { 110 | if (previouslyExistingIds.indexOf(accountId) == -1) { 111 | row.splice(2, 3); 112 | entities.push(new Entity(row)); 113 | previouslyExistingIds.push(accountId); 114 | } 115 | } else { 116 | row.splice(4, 1); 117 | entities.push(new Entity(row)); 118 | } 119 | } 120 | return entities; 121 | } 122 | 123 | /** 124 | * Creates user access report headers based on the selected dimensions and 125 | * metrics. 126 | * @param {!Array>} dimensions An array of objects containing 127 | * the dimension names. 128 | * @param {!Array>} metrics An array of objects containing 129 | * the metric names. 130 | * @param {number} numberOfDateRanges The number of date ranges. 131 | * @return {!Array} An array of header values. 132 | */ 133 | function createUserAccessReportHeaders( 134 | dimensions, metrics, numberOfDateRanges) { 135 | const dimensionNameArray = dimensions.map((value) => value.dimensionName); 136 | const metricNameArray = metrics.map((value) => value.metricName); 137 | if (numberOfDateRanges == 1) { 138 | return [ 139 | 'Report Account', 140 | 'Report Account ID', 141 | 'Report Property', 142 | 'Report Property ID', 143 | ...dimensionNameArray, 144 | ...metricNameArray, 145 | ]; 146 | } else { 147 | return [ 148 | 'Report Account', 149 | 'Report Account ID', 150 | 'Report Property', 151 | 'Report Property ID', 152 | ...dimensionNameArray, 153 | 'Date Range Data Accessed', 154 | ...metricNameArray, 155 | ]; 156 | } 157 | } 158 | 159 | /** 160 | * Gets the user access reports and writes the access records to the "User 161 | * Access Report" sheet after first clearing the contents of that sheet. 162 | */ 163 | function writeUserAccessReportDataToSheet() { 164 | const requestSettings = getUserAccessReportRequestSettingsFromSheet(); 165 | const entities = createUserAccessReportSelectedEntitiesArray( 166 | getSelectedGa4Properties(), requestSettings); 167 | const records = getAllUserAccessRecords(entities, requestSettings); 168 | const headers = createUserAccessReportHeaders( 169 | requestSettings.dimensions, 170 | requestSettings.metrics, 171 | requestSettings.dateRanges.length); 172 | const data = [headers, ...records]; 173 | const sheet = ss.getSheetByName(sheetsMeta.ga4.userAccessReport.sheetName); 174 | sheet.clearContents(); 175 | sheet.getRange(1, 1, data.length, data[0].length).setValues(data); 176 | } -------------------------------------------------------------------------------- /shared/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * The prefix for an entity name. 19 | * @enum {string} 20 | */ 21 | const ENTITY_PREFIX = { 22 | ACCOUNTS: 'accounts/', 23 | PROPERTIES: 'properties/' 24 | }; -------------------------------------------------------------------------------- /shared/interfaces.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * The user access report settings based on the values from the "User Access 19 | * Report Settings" sheet. 20 | * @interface 21 | */ 22 | class UserAccessReportRequestSettings { 23 | /** 24 | * @param {!Array} sheetValues An array of various user 25 | * access report request settings. 26 | */ 27 | constructor(sheetValues) { 28 | /** @type {!Array>} The dimensions requested and displayed 29 | * in the response. 30 | */ 31 | this.dimensions = sheetValues[4].split(',').map( 32 | (value) => ({dimensionName: value.trim()})); 33 | /** @type {!Array>} The metrics requested and displayed in 34 | * the response. 35 | */ 36 | this.metrics = sheetValues[5].split(',').map( 37 | (value) => ({metricName: value.trim()})); 38 | /** @type {string} The row count of the start row. */ 39 | this.offset = sheetValues[6].toString().trim() || '0'; 40 | /** @type {string} The number of rows to return. */ 41 | this.limit = sheetValues[7].toString().trim() || '10000'; 42 | /** @type {string} This request's time zone if specified. */ 43 | this.timeZone = sheetValues[8].trim(); 44 | /** @type {boolean} Determines whether to include users who have never made 45 | * an API call in the response. 46 | */ 47 | this.includeAllUsers = sheetValues[9]; 48 | /** 49 | * @type {boolean} Decides whether to return the users within user groups. 50 | * This field works only when includeAllUsers is set to true. 51 | */ 52 | this.expandGroups = sheetValues[10]; 53 | /** @type {boolean} Decides if the request is made at the account level. */ 54 | this.accountLevelReport = sheetValues[11]; 55 | /** @type {boolean} Toggles whether to return the current state of this 56 | * Analytics Property's quota. 57 | */ 58 | this.returnEntityQuota = false; 59 | /** @type {!Array} Date ranges of access records to read. */ 60 | this.dateRanges = [new DateRange( 61 | sheetValues[0], sheetValues[1], this.timeZone)]; 62 | if (sheetValues[2].toString().length > 0) { 63 | this.dateRanges.push( 64 | new DateRange(sheetValues[2], sheetValues[3], this.timeZone)); 65 | } 66 | } 67 | } 68 | 69 | /** 70 | * An entity that contains the account, property, and stream names and IDs 71 | * based on selections from the account summaries or data stream selection 72 | * sheets. 73 | * @interface 74 | */ 75 | class Entity { 76 | /** 77 | * @param {!Array} sheetValues An array of account, property, and 78 | * stream data from the sheet row. 79 | */ 80 | constructor(sheetValues) { 81 | /** @type {stirng} The account name for an entity. */ 82 | this.accountName = sheetValues[0]; 83 | /** @type {stirng} The account ID for an entity. */ 84 | this.accountId = sheetValues[1]; 85 | /** @type {stirng} The property name for an entity. */ 86 | this.propertyName = sheetValues[2] || ''; 87 | /** @type {stirng} The property ID for an entity. */ 88 | this.propertyId = sheetValues[3] || ''; 89 | /** @type {stirng} The stream name for an entity. */ 90 | this.streamName = sheetValues[4] || ''; 91 | /** @type {stirng} The stream ID for an entity. */ 92 | this.streamId = sheetValues[5] || ''; 93 | } 94 | } 95 | 96 | /** 97 | * Start and end dates in YYYY-MM-DD format. 98 | * @interface 99 | */ 100 | class DateRange { 101 | /** 102 | * @param {!Date} startDate The date range start date. 103 | * @param {!Date} endDate The date range end date. 104 | * @param {string} timeZone The date range time zone. 105 | */ 106 | constructor(startDate, endDate, timeZone) { 107 | /** @type {string} The start date in YYYY-MM-DD format. */ 108 | this.startDate = Utilities.formatDate(startDate, timeZone, 'yyyy-MM-dd'); 109 | /** @type {string} The end date in YYYY-MM-DD format. */ 110 | this.endDate = Utilities.formatDate(endDate, timeZone, 'yyyy-MM-dd'); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /shared/menu.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | function onOpen(e) { 17 | const ui = SpreadsheetApp.getUi(); 18 | ui.createMenu('Google Analytics Utilities') 19 | // Selectors 20 | .addItem('List Account Summaries', 'writeGA4AccountSummariesToSheet') 21 | .addItem('List Data Stream Selection', 'writeDataStreamSelectionToSheet') 22 | .addSubMenu( 23 | ui.createMenu('Properties') 24 | .addItem('List', 'writeGA4PropertyDetailsToSheet') 25 | .addItem('Modify', 'modifyGA4Properties')) 26 | .addSubMenu( 27 | ui.createMenu('Users') 28 | .addItem('List', 'writeGA4AccessBindingsToSheet') 29 | .addItem('Modify', 'modifyGA4AccessBindings')) 30 | .addSubMenu( 31 | ui.createMenu('Audiences') 32 | .addItem('List', 'writeGA4AudiencesToSheet') 33 | .addItem('Modify', 'modifyGA4Audiences')) 34 | .addSubMenu( 35 | ui.createMenu('Channel Groups') 36 | .addItem('List', 'writeGA4ChannelGroupsToSheet') 37 | .addItem('Modify', 'modifyChannelGroups')) 38 | .addSubMenu( 39 | ui.createMenu('Custom Dimensions') 40 | .addItem('List', 'writeGA4CustomDimensionsToSheet') 41 | .addItem('Modify', 'modifyGA4CustomDimensions')) 42 | .addSubMenu( 43 | ui.createMenu('Custom Metrics') 44 | .addItem('List', 'writeGA4CustomMetricsToSheet') 45 | .addItem('Modify', 'modifyGA4CustomMetrics')) 46 | .addSubMenu( 47 | ui.createMenu('Calculated Metrics') 48 | .addItem('List', 'writeGA4CalculatedMetricsToSheet') 49 | .addItem('Modify', 'modifyCalculatedMetrics')) 50 | .addSubMenu( 51 | ui.createMenu('Key Events') 52 | .addItem('List', 'writeGA4KeyEventsToSheet') 53 | .addItem('Modify', 'modifyGA4KeyEvents')) 54 | .addSubMenu( 55 | ui.createMenu('Expanded Data Sets') 56 | .addItem('List', 'writeGA4ExpandedDataSetsToSheet') 57 | .addItem('Modify', 'modifyGA4ExpandedDataSets')) 58 | // Submenu for GA4 property link integrations 59 | .addSubMenu( 60 | ui.createMenu('Links') 61 | .addSubMenu( 62 | ui.createMenu('Google Ads') 63 | .addItem('List', 'writeGA4AdsLinksToSheet') 64 | .addItem('Modify', 'modifyGA4AdsLinks')) 65 | .addSubMenu( 66 | ui.createMenu('Firebase') 67 | .addItem('List', 'writeGA4FirebaseLinksToSheet') 68 | .addItem('Modify', 'modifyGA4FirebaseLinks')) 69 | .addSubMenu( 70 | ui.createMenu('DV360') 71 | .addItem('List', 'writeGA4DV360LinksToSheet') 72 | .addItem('Modify', 'modifyGA4DV360Links')) 73 | .addSubMenu( 74 | ui.createMenu('SA360') 75 | .addItem('List', 'writeGA4SA360LinksToSheet') 76 | .addItem('Modify', 'modifyGA4SA360Links')) 77 | .addSubMenu( 78 | ui.createMenu('BigQuery') 79 | .addItem('List', 'writeGA4BigQueryLinksToSheet') 80 | .addItem('Modify', 'modifyBigQueryLinksToSheet')) 81 | .addSubMenu( 82 | ui.createMenu('AdSense') 83 | .addItem('List', 'writeGA4AdSenseLinksToSheet') 84 | .addItem('Modify', 'modifyAdSenseLinks')) 85 | ) 86 | .addSubMenu( 87 | ui.createMenu('Data Streams') 88 | .addItem('List', 'writeGA4StreamsToSheet') 89 | .addItem('Modify', 'modifyGA4Streams')) 90 | .addSubMenu( 91 | ui.createMenu('Measurement Protocol Secrets') 92 | .addItem('List', 'writeGA4MeasurementProtocolSecretsToSheet') 93 | .addItem('Modify', 'modifyMeasurementProtocolSecrets')) 94 | .addSubMenu( 95 | ui.createMenu('Event Create Rules') 96 | .addItem('List', 'writeGA4EventCreateRulesToSheet') 97 | .addItem('Modify', 'modifyEventCreateRules')) 98 | .addSubMenu( 99 | ui.createMenu('Event Edit Rules') 100 | .addItem('List', 'writeEventEditRulesToSheet') 101 | .addItem('Modify', 'modifyEventEditRules')) 102 | .addSubMenu( 103 | ui.createMenu('Subproperty Event Filters') 104 | .addItem('List', 'writeGA4SubpropertyEventFiltersToSheet') 105 | .addItem('Modify', 'modifySubpropertyEventFilters')) 106 | .addSubMenu( 107 | ui.createMenu('Rollup Property Source Links') 108 | .addItem('List', 'writeGA4RollupPropertySourceLinksToSheet') 109 | .addItem('Modify', 'modifyRollupPropertySourceLinks')) 110 | .addSeparator() 111 | .addSubMenu( 112 | ui.createMenu('Advanced') 113 | .addSubMenu( 114 | ui.createMenu('Easy Property Creation') 115 | .addItem('List Templates', 'writePropertyTemplatesToSheet') 116 | .addItem('Create Properties', 'createPropertiesFromTemplates') 117 | .addItem( 118 | 'Resize Row Heights', 'resizeEasyPropertyCreationSheetRowHeights')) 119 | .addSubMenu( 120 | ui.createMenu('Health Report') 121 | .addItem('Create Report', 'createHealthReport')) 122 | .addSubMenu( 123 | ui.createMenu('Audience Lists') 124 | .addItem('List Existing Audiences', 'writeGA4AudiencesToAudiencesListsSheet') 125 | .addItem('List Audience Lists', 'writeAudienceListsToSheet') 126 | .addItem('Create Audience Lists', 'createAudienceLists') 127 | .addItem('Check Audience List States', 'checkAudienceListsState') 128 | .addItem('Export Audience Lists', 'exportAudienceListsData')) 129 | .addSubMenu( 130 | ui.createMenu('User Access Report') 131 | .addItem('Run Report', 'writeUserAccessReportDataToSheet'))) 132 | .addItem('List All Property Settings', 'listAllGA4PropertyResources') 133 | .addSeparator() 134 | .addItem('Check for Updates', 'checkRelease') 135 | .addToUi(); 136 | } -------------------------------------------------------------------------------- /shared/shared.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const ss = SpreadsheetApp.getActive(); 18 | const apiActionTaken = { 19 | ga4: { 20 | deleted: 'Deleted', 21 | created: 'Created', 22 | archived: 'Archived', 23 | skipped: 'Skipped', 24 | error: 'Error', 25 | updated: 'Updated' 26 | } 27 | }; 28 | 29 | /** 30 | * Lists all values across all GA4 sheets. 31 | */ 32 | function listAllGA4PropertyResources() { 33 | writeGA4AudiencesToSheet(); 34 | writeGA4KeyEventsToSheet(); 35 | writeGA4StreamsToSheet(); 36 | writeGA4PropertyDetailsToSheet(); 37 | writeGA4CustomDimensionsToSheet(); 38 | writeGA4CustomMetricsToSheet(); 39 | writeGA4CalculatedMetricsToSheet(); 40 | writeGA4AdsLinksToSheet(); 41 | writeGA4DV360LinksToSheet(); 42 | writeGA4FirebaseLinksToSheet(); 43 | writeGA4SA360LinksToSheet(); 44 | writeGA4BigQueryLinksToSheet(); 45 | writeGA4AdSenseLinksToSheet(); 46 | writeGA4ChannelGroupsToSheet(); 47 | writeGA4ExpandedDataSetsToSheet(); 48 | writeGA4AdSenseLinksToSheet(); 49 | writeGA4EventCreateRulesToSheet(); 50 | writeGA4RollupPropertySourceLinksToSheet(); 51 | writeGA4SubpropertyEventFiltersToSheet(); 52 | } 53 | 54 | /** 55 | * Returns either the read or write range for a given sheet. 56 | * @param {string} name The name of the sheet for the range. 57 | * @param {string} type Either read or write. 58 | * @return {!Object|null} The specified range object. 59 | */ 60 | function getSheetRange(name, type) { 61 | for (n in sheetsMeta.ga4) { 62 | if (name == sheetsMeta.ga4[n].sheetName) { 63 | return sheetsMeta.ga4[n][type]; 64 | } 65 | } 66 | for (n in sheetsMeta.ua) { 67 | if (name == sheetsMeta.ua[n].sheetName) { 68 | return sheetsMeta.ua[n][type]; 69 | } 70 | } 71 | return null; 72 | } 73 | /** 74 | * Returns an array of selected properties based on the selected views. 75 | * @param {!Array} selectedViews An array of the selected views. 76 | * @return {!Array} A deduplicated list of selected properties based on 77 | * the selected views. 78 | */ 79 | function getSelectedProperties(selectedViews) { 80 | return selectedViews.filter((row, index) => { 81 | if (index == 0) { 82 | return row; 83 | } else if (row[3] != selectedViews[index - 1][3]) { 84 | return row; 85 | } 86 | }); 87 | } 88 | /** 89 | * Returns a double array of all properties in all accounts. 90 | * @return {!Array} A double array of all propertiesunder all accounts a 91 | * user has access to. 92 | */ 93 | function getAllProperties() { 94 | const finalProperties = []; 95 | const summaries = getAccountSummaries(); 96 | for (let i = 0; i < summaries.items.length; i++) { 97 | const accountName = summaries.items[i].name; 98 | const accountId = summaries.items[i].id; 99 | const properties = summaries.items[i].webProperties; 100 | if (properties !== undefined) { 101 | for (let j = 0; j < properties.length; j++) { 102 | const propertyName = properties[j].name; 103 | const propertyId = properties[j].id; 104 | let is360 = false; 105 | if (properties[j].level == 'PREMIUM') { 106 | is360 = true; 107 | } 108 | finalProperties.push( 109 | [accountName, accountId, propertyName, propertyId, is360]); 110 | } 111 | } 112 | } 113 | return finalProperties; 114 | } 115 | /** 116 | * Returns a double array of all properties in all accounts. 117 | * @return {!Array} A double array of all properties under all accounts 118 | * a user has access to. 119 | */ 120 | function getAllPropertiesWithInternalIds() { 121 | const finalProperties = []; 122 | const summaries = getAccountSummaries(); 123 | for (let i = 0; i < summaries.items.length; i++) { 124 | const accountName = summaries.items[i].name; 125 | const accountId = summaries.items[i].id; 126 | const properties = summaries.items[i].webProperties; 127 | if (properties !== undefined) { 128 | for (let j = 0; j < properties.length; j++) { 129 | const propertyName = properties[j].name; 130 | const propertyId = properties[j].id; 131 | const internalPropertyId = properties[j].internalWebPropertyId; 132 | let is360 = false; 133 | if (properties[j].level == 'PREMIUM') { 134 | is360 = true; 135 | } 136 | finalProperties.push( 137 | [accountName, accountId, propertyName, propertyId, 138 | internalPropertyId, is360]); 139 | } 140 | } 141 | } 142 | return finalProperties; 143 | } 144 | /** 145 | * Writes data to a specified sheet. 146 | * @param {!Array} data The data to be written to the sheet. 147 | * @param {string} sheetName The name of the sheet to which the data will be 148 | * written. 149 | */ 150 | function writeToSheet(data, sheetName) { 151 | const ranges = getSheetRange(sheetName, 'write'); 152 | let sheet = ss.getSheetByName(sheetName); 153 | if (sheet == undefined) { 154 | sheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet(sheetName); 155 | } 156 | if (data.length > 0) { 157 | sheet.getRange(ranges.row, ranges.column, data.length, ranges.numColumns) 158 | .setValues(data); 159 | } 160 | } 161 | /** 162 | * Retrieves data from a specified sheet. 163 | * @param {string} sheetName The sheet name where the data is located. 164 | * @return {!Array} A two dimensional array of the rows of data 165 | * retrieved from the sheet. 166 | */ 167 | function getDataFromSheet(sheetName) { 168 | const ranges = getSheetRange(sheetName, 'read'); 169 | let sheet = ss.getSheetByName(sheetName); 170 | return sheet.getRange( 171 | ranges.row, ranges.column, 172 | sheet.getLastRow() - ranges.row + 1, ranges.numColumns).getValues(); 173 | } 174 | /** 175 | * Get selected properties from the account summaries sheet based 176 | * on which views have a checked box. 177 | * @return {!Array} A two dimensional array of the selected 178 | * properties retrieved from the sheet. 179 | */ 180 | function getSelectedGa4Properties() { 181 | const properties = getDataFromSheet(sheetsMeta.ga4.accountSummaries.sheetName); 182 | return properties.filter(row => row[4]); 183 | } 184 | 185 | /** 186 | * Writes the action that was taken on an entity to a sheet. 187 | * @param {string} sheetName The name of the sheet being written to. 188 | * @param {number} index The index of the entity that is being acted 189 | * upon in the two dimensional array of entity data. 190 | * @param {string} status The action taken for a given entity. 191 | */ 192 | function writeActionTakenToSheet(sheetName, index, actionTaken) { 193 | // The actual row to be written to is offset from the index value by 2, so 194 | // the index value must be increased by two. 195 | const writeRow = index + 2; 196 | const actionTakenColumn = ss.getSheetByName(sheetName).getLastColumn(); 197 | const numRows = 1; 198 | const numColumns = 1; 199 | ss.getSheetByName(sheetName).getRange( 200 | writeRow, actionTakenColumn, numRows, numColumns 201 | ).setValue(actionTaken); 202 | } 203 | 204 | /** 205 | * Clears a sheet of content. 206 | * @param {!Object} sheetsMetaField The object from the sheetsMeta variable. 207 | */ 208 | function clearSheetContent(sheetsMetaField) { 209 | const sheet = SpreadsheetApp.getActive().getSheetByName( 210 | sheetsMetaField.sheetName); 211 | sheet.getRange( 212 | 2, 1, sheet.getLastRow(), sheet.getLastColumn() 213 | ).clearContent(); 214 | } 215 | 216 | /** 217 | * Builds a GA4 report object. 218 | * @param {!Array} dimensions Dimensions requested in the report. 219 | * @param {!Array} metrics Metrics requested in the report. 220 | * @param {string} startDate Start date for the report. 221 | * @param {string} endDate End date for the report. 222 | * @param {!Object} dimensionFilter Dimension filters for the report. 223 | * @returns {!Object} Analytics report object. 224 | */ 225 | function generateGA4DataReportRequest(dimensions, metrics, startDate, endDate, dimensionFilter) { 226 | const request = AnalyticsData.newRunReportRequest(); 227 | // Sets metrics. 228 | if (metrics.length > 0) { 229 | const reportMetrics = []; 230 | metrics.forEach(metric => { 231 | const newMetric = AnalyticsData.newMetric(); 232 | newMetric.name = metric; 233 | reportMetrics.push(newMetric); 234 | }); 235 | request.metrics = reportMetrics; 236 | } 237 | // Sets dimensions. 238 | if (dimensions.length > 0) { 239 | const reportDimensions = []; 240 | dimensions.forEach(dimension => { 241 | const newDimension = AnalyticsData.newDimension(); 242 | newDimension.name = dimension; 243 | reportDimensions.push(newDimension); 244 | }); 245 | request.dimensions = reportDimensions; 246 | } 247 | // Sets date range. 248 | const dateRange = AnalyticsData.newDateRange(); 249 | dateRange.startDate = startDate; 250 | dateRange.endDate = endDate; 251 | request.dateRanges = dateRange; 252 | // Sets dimension filter. 253 | if (Object.keys(dimensionFilter).length > 0) { 254 | const newFilter = AnalyticsData.newFilterExpression(); 255 | newFilter.filter = AnalyticsData.newFilter(); 256 | newFilter.filter.fieldName = dimensionFilter.name; 257 | newFilter.filter.stringFilter = AnalyticsData.newStringFilter(); 258 | newFilter.filter.stringFilter.matchType = dimensionFilter.matchType; 259 | newFilter.filter.stringFilter.value = dimensionFilter.value; 260 | request.dimensionFilter = newFilter; 261 | } 262 | return request; 263 | } 264 | 265 | /** 266 | * Auto-resize all row heights for a sheet. 267 | * @param {string} sheetName 268 | * @param {number} rowHeight Row height in pixels. 269 | */ 270 | function resizeRowHeights(sheetName, rowHeight) { 271 | const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName); 272 | sheet.setRowHeightsForced(2, sheet.getLastRow() - 1, rowHeight); 273 | } 274 | 275 | /** 276 | * List properties, accounts, and data streams in an array to be used for 277 | * selection purposes. 278 | */ 279 | function writeDataStreamSelectionToSheet() { 280 | const streamArray = []; 281 | const selectedProperties = getSelectedGa4Properties(); 282 | for (let i = 0; i < selectedProperties.length; i++) { 283 | const currentRow = selectedProperties[i]; 284 | const propertyName = `properties/${currentRow[3]}`; 285 | const dataStreams = listGA4Entities('streams', propertyName).dataStreams; 286 | if (dataStreams) { 287 | for (let j = 0; j < dataStreams.length; j++) { 288 | const currentStream = dataStreams[j]; 289 | streamArray.push([ 290 | currentRow[0], 291 | currentRow[1], 292 | currentRow[2], 293 | currentRow[3], 294 | currentStream.displayName, 295 | currentStream.name.split('dataStreams/')[1]] 296 | ); 297 | } 298 | } 299 | } 300 | writeToSheet(streamArray, sheetsMeta.ga4.dataStreamSelection.sheetName); 301 | } 302 | /** 303 | * Get selected data streams from the data stream selection sheet based. 304 | * @return {!Array} A two dimensional array of the selected 305 | * stream retrieved from the sheet. 306 | */ 307 | function getSelectedGa4DataStreams() { 308 | const streams = getDataFromSheet( 309 | sheetsMeta.ga4.dataStreamSelection.sheetName); 310 | return streams.filter(row => row[4]); 311 | } -------------------------------------------------------------------------------- /shared/sheetsMeta.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const sheetsMeta = { 18 | ga4: { 19 | accountSummaries: { 20 | sheetName: 'Account Summaries', 21 | write: { 22 | row: 2, 23 | column: 1, 24 | numRows: 1, 25 | numColumns: 4 26 | }, 27 | read: { 28 | row: 2, 29 | column: 1, 30 | numRows: 1, 31 | numColumns: 5 32 | } 33 | }, 34 | streams: { 35 | sheetName: 'Data Streams', 36 | write: { 37 | row: 2, 38 | column: 1, 39 | numRows: 1, 40 | numColumns: 24 41 | }, 42 | read: { 43 | row: 2, 44 | column: 1, 45 | numRows: 1, 46 | numColumns: 28 47 | } 48 | }, 49 | customDimensions: { 50 | sheetName: 'Custom Dimensions', 51 | write: { 52 | row: 2, 53 | column: 1, 54 | numRows: 1, 55 | numColumns: 10 56 | }, 57 | read: { 58 | row: 2, 59 | column: 1, 60 | numRows: 1, 61 | numColumns: 14 62 | } 63 | }, 64 | customMetrics: { 65 | sheetName: 'Custom Metrics', 66 | write: { 67 | row: 2, 68 | column: 1, 69 | numRows: 1, 70 | numColumns: 11 71 | }, 72 | read: { 73 | row: 2, 74 | column: 1, 75 | numRows: 1, 76 | numColumns: 15 77 | } 78 | }, 79 | keyEvents: { 80 | sheetName: 'Key Events', 81 | write: { 82 | row: 2, 83 | column: 1, 84 | numRows: 1, 85 | numColumns: 12 86 | }, 87 | read: { 88 | row: 2, 89 | column: 1, 90 | numRows: 1, 91 | numColumns: 16 92 | } 93 | }, 94 | googleAdsLinks: { 95 | sheetName: 'Google Ads Links', 96 | write: { 97 | row: 2, 98 | column: 1, 99 | numRows: 1, 100 | numColumns: 11 101 | }, 102 | read: { 103 | row: 2, 104 | column: 1, 105 | numRows: 1, 106 | numColumns: 15 107 | } 108 | }, 109 | firebaseLinks: { 110 | sheetName: 'Firebase Links', 111 | write: { 112 | row: 2, 113 | column: 1, 114 | numRows: 1, 115 | numColumns: 7 116 | }, 117 | read: { 118 | row: 2, 119 | column: 1, 120 | numRows: 1, 121 | numColumns: 11 122 | } 123 | }, 124 | displayVideo360AdvertiserLinks: { 125 | sheetName: 'DV360 Links', 126 | write: { 127 | row: 2, 128 | column: 1, 129 | numRows: 1, 130 | numColumns: 10 131 | }, 132 | read: { 133 | row: 2, 134 | column: 1, 135 | numRows: 1, 136 | numColumns: 14 137 | } 138 | }, 139 | copyProperties: { 140 | sheetName: 'Copy Properties', 141 | write: { 142 | row: 2, 143 | column: 1, 144 | numRows: 1, 145 | numColumns: 6 146 | }, 147 | read: { 148 | row: 2, 149 | column: 1, 150 | numRows: 1, 151 | numColumns: 21 152 | } 153 | }, 154 | properties: { 155 | sheetName: 'Property Details', 156 | write: { 157 | row: 2, 158 | column: 1, 159 | numRows: 1, 160 | numColumns: 21 161 | }, 162 | read: { 163 | row: 2, 164 | column: 1, 165 | numRows: 1, 166 | numColumns: 25 167 | } 168 | }, 169 | audiences: { 170 | sheetName: 'Audiences', 171 | write: { 172 | row: 2, 173 | column: 1, 174 | numRows: 1, 175 | numColumns: 13 176 | }, 177 | read: { 178 | row: 2, 179 | column: 1, 180 | numRows: 1, 181 | numColumns: 17 182 | } 183 | }, 184 | accessBindings: { 185 | sheetName: 'Users', 186 | write: { 187 | row: 2, 188 | column: 1, 189 | numRows: 1, 190 | numColumns: 9 191 | }, 192 | read: { 193 | row: 2, 194 | column: 1, 195 | numRows: 1, 196 | numColumns: 13 197 | } 198 | }, 199 | healthReport: { 200 | sheetName: 'Report Settings', 201 | read: { 202 | row: 4, 203 | column: 1, 204 | numRows: 1, 205 | numColumns: 4 206 | } 207 | }, 208 | bigqueryLinks: { 209 | sheetName: 'BigQuery Links', 210 | read: { 211 | row: 2, 212 | column: 1, 213 | numRows: 1, 214 | numColumns: 18 215 | }, 216 | write: { 217 | row: 2, 218 | column: 1, 219 | numRows: 1, 220 | numColumns: 14 221 | } 222 | }, 223 | sa360Links: { 224 | sheetName: 'SA360 Links', 225 | read: { 226 | row: 2, 227 | column: 1, 228 | numRows: 1, 229 | numColumns: 14 230 | }, 231 | write: { 232 | row: 2, 233 | column: 1, 234 | numRows: 1, 235 | numColumns: 10 236 | } 237 | }, 238 | expandedDataSets: { 239 | sheetName: 'Expanded Data Sets', 240 | read: { 241 | row: 2, 242 | column: 1, 243 | numRows: 1, 244 | numColumns: 15 245 | }, 246 | write: { 247 | row: 2, 248 | column: 1, 249 | numRows: 1, 250 | numColumns: 11 251 | } 252 | }, 253 | fullPropertyDeployment: { 254 | sheetName: 'Easy Property Creation', 255 | read: { 256 | row: 2, 257 | column: 1, 258 | numRows: 1, 259 | numColumns: 25 260 | }, 261 | write: { 262 | row: 2, 263 | column: 1, 264 | numRows: 1, 265 | numColumns: 20 266 | } 267 | }, 268 | connectedSiteTags: { 269 | sheetName: 'UA Connected Site Tags', 270 | write: { 271 | row: 2, 272 | column: 1, 273 | numRows: 1, 274 | numColumns: 7 275 | }, 276 | read: { 277 | row: 2, 278 | column: 1, 279 | numRows: 1, 280 | numColumns: 11 281 | } 282 | }, 283 | channelGroups: { 284 | sheetName: 'Channel Groups', 285 | write: { 286 | row: 2, 287 | column: 1, 288 | numRows: 1, 289 | numColumns: 10 290 | }, 291 | read: { 292 | row: 2, 293 | column: 1, 294 | numRows: 1, 295 | numColumns: 14 296 | } 297 | }, 298 | dataStreamSelection: { 299 | sheetName: 'Data Stream Selection', 300 | write: { 301 | row: 2, 302 | column: 1, 303 | numRows: 1, 304 | numColumns: 6 305 | }, 306 | read: { 307 | row: 2, 308 | column: 1, 309 | numRows: 1, 310 | numColumns: 7 311 | } 312 | }, 313 | measurementProtocolSecrets: { 314 | sheetName: 'Measurement Protocol Secrets', 315 | write: { 316 | row: 2, 317 | column: 1, 318 | numRows: 1, 319 | numColumns: 9 320 | }, 321 | read: { 322 | row: 2, 323 | column: 1, 324 | numRows: 1, 325 | numColumns: 13 326 | } 327 | }, 328 | adSenseLinks: { 329 | sheetName: 'AdSense Links', 330 | write: { 331 | row: 2, 332 | column: 1, 333 | numRows: 1, 334 | numColumns: 6 335 | }, 336 | read: { 337 | row: 2, 338 | column: 1, 339 | numRows: 1, 340 | numColumns: 10 341 | } 342 | }, 343 | eventCreateRules: { 344 | sheetName: 'Event Create Rules', 345 | write: { 346 | row: 2, 347 | column: 1, 348 | numRows: 1, 349 | numColumns: 11 350 | }, 351 | read: { 352 | row: 2, 353 | column: 1, 354 | numRows: 1, 355 | numColumns: 15 356 | } 357 | }, 358 | eventEditRules: { 359 | sheetName: 'Event Edit Rules', 360 | write: { 361 | row: 2, 362 | column: 1, 363 | numRows: 1, 364 | numColumns: 11 365 | }, 366 | read: { 367 | row: 2, 368 | column: 1, 369 | numRows: 1, 370 | numColumns: 15 371 | } 372 | }, 373 | audienceLists: { 374 | sheetName: 'Audience Lists', 375 | write: { 376 | row: 2, 377 | column: 1, 378 | numRows: 1, 379 | numColumns: 11 380 | }, 381 | read: { 382 | row: 2, 383 | column: 1, 384 | numRows: 1, 385 | numColumns: 21 386 | } 387 | }, 388 | rollupPropertySourceLinks: { 389 | sheetName: 'Rollup Property Source Links', 390 | write: { 391 | row: 2, 392 | column: 1, 393 | numRows: 1, 394 | numColumns: 6 395 | }, 396 | read: { 397 | row: 2, 398 | column: 1, 399 | numRows: 1, 400 | numColumns: 10 401 | } 402 | }, 403 | subpropertyEventFilters: { 404 | sheetName: 'Subproperty Event Filters', 405 | write: { 406 | row: 2, 407 | column: 1, 408 | numRows: 1, 409 | numColumns: 7 410 | }, 411 | read: { 412 | row: 2, 413 | column: 1, 414 | numRows: 1, 415 | numColumns: 11 416 | } 417 | }, 418 | calculatedMetrics: { 419 | sheetName: 'Calculated Metrics', 420 | write: { 421 | row: 2, 422 | column: 1, 423 | numRows: 1, 424 | numColumns: 12 425 | }, 426 | read: { 427 | row: 2, 428 | column: 1, 429 | numRows: 1, 430 | numColumns: 16 431 | } 432 | }, 433 | userAccessReport: { 434 | sheetName: 'User Access Report', 435 | write: { 436 | row: 1, 437 | column: 1, 438 | numRows: 1, 439 | numColumns: 1 440 | }, 441 | read: { 442 | row: 1, 443 | column: 1, 444 | numRows: 1, 445 | numColumns: 1 446 | } 447 | }, 448 | userAccessReportSettings: { 449 | sheetName: 'User Access Report Settings', 450 | write: { 451 | row: 1, 452 | column: 1, 453 | numRows: 1, 454 | numColumns: 1 455 | }, 456 | read: { 457 | row: 1, 458 | column: 1, 459 | numRows: 1, 460 | numColumns: 1 461 | } 462 | } 463 | } 464 | }; -------------------------------------------------------------------------------- /utils/checks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const messageText = { 18 | newRelease: ` 19 | There is a new version of this tool available. Please use the latest version 20 | of the tool by using the files on Github or making a copy of this spreadsheet: 21 | https://docs.google.com/spreadsheets/d/1kJqwYNed8RTuAgjy0aRUooD__MIPqzUeiDF5LZ7v1aI/copy 22 | 23 | Update Details: 24 | ` 25 | } 26 | 27 | /** 28 | * Checks if this is the latest version of the script and sheet. 29 | * If not, it prompts the user to create a new copy of the sheet 30 | * from Github. 31 | */ 32 | function checkRelease() { 33 | const ui = SpreadsheetApp.getUi(); 34 | const settingsSheet = ss.getSheetByName('Settings'); 35 | 36 | // Get sheet version. 37 | const rawReleaseVersion = settingsSheet.getRange(1, 2, 1, 1).getValue(); 38 | const sheetReleaseVersion = parseFloat(rawReleaseVersion.split('v')[1]); 39 | 40 | // Get Github version. 41 | const releases = JSON.parse( 42 | UrlFetchApp.fetch( 43 | 'https://api.github.com/repos/google/google-analytics-utilities/releases' 44 | ).getContentText()); 45 | const latestGithubRelease = releases[0]; 46 | const latestGithubVersion = parseFloat( 47 | latestGithubRelease.tag_name.split('v')[1]); 48 | 49 | if (sheetReleaseVersion < latestGithubVersion) { 50 | const title = 'Update Avilable'; 51 | const message = messageText.newRelease + latestGithubRelease.body + ` 52 | 53 | ` + latestGithubRelease.html_url; 54 | ui.alert(title, message, ui.ButtonSet.OK); 55 | } else { 56 | ui.alert('No updates avaialable.'); 57 | } 58 | } 59 | 60 | /** 61 | * Check if a write response has an error and returns error information or 62 | * the action taken. 63 | * @param {!Array} responses The response to the write request. 64 | * @param {string} requestType The kind of request that was made. 65 | * @return {string} Either the action taken or error information. 66 | */ 67 | function responseCheck(responses, requestType) { 68 | const output = []; 69 | responses.forEach(response => { 70 | if (response.details != undefined) { 71 | output.push('Error ' + response.details.code + ': ' + 72 | response.details.message); 73 | } else if (response.statusCode != undefined) { 74 | output.push('Error ' + response.statusCode + ': ' + response.name); 75 | } else { 76 | if (requestType == 'create') { 77 | if (response.name) { 78 | output.push(apiActionTaken.ga4.created + ': ' + response.name); 79 | } else { 80 | output.push(apiActionTaken.ga4.created); 81 | } 82 | } else if (requestType == 'update') { 83 | if (response.measurementUnit == 'CURRENCY') { 84 | output.push(apiActionTaken.ga4.updated + ': ' + response.name + 85 | ' - NOTE: CURRENCY cannot be changed to a different measurement unit.' 86 | ); 87 | } else { 88 | output.push(apiActionTaken.ga4.updated + ': ' + response.name); 89 | } 90 | } else if (requestType == 'archive') { 91 | output.push(apiActionTaken.ga4.archived); 92 | } else if (requestType == 'delete') { 93 | output.push(apiActionTaken.ga4.deleted); 94 | } 95 | } 96 | }); 97 | return output.join('\n'); 98 | } --------------------------------------------------------------------------------