├── 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 | }
--------------------------------------------------------------------------------