ga4-export-fixer 0.1.6-dev.1 → 0.1.6-dev.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -220,6 +220,64 @@ All fields are optional except `sourceTable`. Default values are applied automat
220
220
 
221
221
  Date fields (`dateRangeStart`, `dateRangeEnd`, etc.) accept string dates in `YYYYMMDD` or `YYYY-MM-DD` format, or BigQuery SQL expressions (e.g. `'current_date()'`, `'date(2026, 1, 1)'`).
222
222
 
223
+ ### Building on top of the ga4_events_enhanced table
224
+
225
+ **`definitions/ga4/ga4_sessions.sqlx`**
226
+ ```javascript
227
+ config {
228
+ type: "incremental",
229
+ description: "GA4 sessions table",
230
+ schema: "ga4_export_fixer",
231
+ bigquery: {
232
+ partitionBy: "event_date",
233
+ clusterBy: ['session_id', 'data_is_final'],
234
+ },
235
+ tags: ['ga4_export_fixer']
236
+ }
237
+
238
+ js {
239
+ const { setPreOperations, helpers } = require('ga4-export-fixer');
240
+
241
+ const config = {
242
+ self: self(),
243
+ incremental: incremental(),
244
+ test: false,
245
+ testConfig: {
246
+ dateRangeStart: 'current_date()-1',
247
+ dateRangeEnd: 'current_date()',
248
+ },
249
+ preOperations: {
250
+ dateRangeStartFullRefresh: 'date(2000, 1, 1)',
251
+ dateRangeEnd: 'current_date()',
252
+ //incrementalStartOverride: undefined,
253
+ //incrementalEndOverride: undefined,
254
+ //numberOfPreviousDaysToScan: 10,
255
+ },
256
+ };
257
+ }
258
+
259
+ select
260
+ event_date,
261
+ session_id,
262
+ user_pseudo_id,
263
+ any_value(session_traffic_source_last_click.cross_channel_campaign) as session_traffic_source,
264
+ any_value(landing_page) as landing_page,
265
+ current_datetime() as row_inserted_timestamp,
266
+ min(data_is_final) as data_is_final
267
+ from
268
+ ${ref('ga4_events_enhanced_298233330')}
269
+ where
270
+ ${helpers.incrementalDateFilter(config)}
271
+ group by
272
+ event_date,
273
+ session_id,
274
+ user_pseudo_id
275
+
276
+ pre_operations {
277
+ ${setPreOperations(config)}
278
+ }
279
+ ```
280
+
223
281
  ### Helpers
224
282
 
225
283
  The helpers contain templates for common SQL expressions. The functions are referenced by **ga4EventsEnhanced** but can also be imported as utility functions for working with GA4 data.
@@ -0,0 +1,85 @@
1
+ /*
2
+ These are the configuration defaults that can be extended.
3
+
4
+ For example, load the defaults in ga4EventsEnhanced.js and then extend them with whatever is psecific to the table.
5
+ After that, extend the configuration further with the user's configuration.
6
+ */
7
+
8
+ /*
9
+ The base configuration. Input config validation should always check these fields.
10
+ */
11
+ const baseConfig = {
12
+ self: undefined,
13
+ incremental: undefined,
14
+ test: false,
15
+ testConfig: {
16
+ dateRangeStart: 'current_date()-1',
17
+ dateRangeEnd: 'current_date()',
18
+ },
19
+ preOperations: {
20
+ dateRangeStartFullRefresh: 'date(2000, 1, 1)',
21
+ dateRangeEnd: 'current_date()',
22
+ // incrementalStartOverride and incrementalEndOverride are used to override the date range start and end for incremental refresh
23
+ // this is useful if you want to re-process only a specific date range
24
+ incrementalStartOverride: undefined,
25
+ incrementalEndOverride: undefined,
26
+ numberOfPreviousDaysToScan: 10,
27
+ },
28
+ };
29
+
30
+ /*
31
+ The default configuration for the GA4 Events Enhanced table.
32
+ */
33
+ const ga4EventsEnhancedConfig = {
34
+ ...baseConfig,
35
+ sourceTable: undefined,
36
+ sourceTableType: 'GA4_EXPORT', // used with pre operations to detect if ga4 export specific pre operations are needed
37
+ // optional but recommended
38
+ schemaLock: undefined,
39
+ // only used with js tables
40
+ // dataformTableConfig: {},
41
+ // optional
42
+ includedExportTypes: {
43
+ daily: true,
44
+ intraday: true,
45
+ fresh: false,
46
+ },
47
+ timezone: 'Etc/UTC',
48
+ customTimestampParam: undefined,
49
+ dataIsFinal: {
50
+ detectionMethod: 'EXPORT_TYPE', // or 'DAY_THRESHOLD'
51
+ dayThreshold: 4 // only used if detectionMethod is 'DAY_THRESHOLD'
52
+ },
53
+ // number of additional days to take in for taking into account sessions that overlap days
54
+ bufferDays: 1,
55
+ // these parameters are excluded by default because they've been made available in other columns
56
+ defaultExcludedEventParams: [
57
+ 'page_location',
58
+ 'ga_session_id',
59
+ //'custom_event_timestamp', // removed if customTimestampParam is used
60
+ ],
61
+ excludedEventParams: [],
62
+ eventParamsToColumns: [
63
+ //{name: 'page_location', type: 'string', columnName: 'page_location2'},
64
+ ],
65
+ sessionParams: [],
66
+ defaultExcludedEvents: [],
67
+ // session_start and first_visit are excluded via the excludedEvents array
68
+ // this allows the user to include them if needed
69
+ excludedEvents: [
70
+ 'session_start',
71
+ 'first_visit'
72
+ ],
73
+ defaultExcludedColumns: [
74
+ 'event_dimensions', // legacy column, not needed
75
+ 'traffic_source', // renamed to user_traffic_source
76
+ 'session_id'
77
+ ],
78
+ // exclude these columns when extracting raw data from the export tables
79
+ excludedColumns: [],
80
+ };
81
+
82
+ module.exports = {
83
+ baseConfig,
84
+ ga4EventsEnhancedConfig,
85
+ };
package/helpers.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const constants = require('./constants');
2
+ const { baseConfig } = require('./defaultConfig');
2
3
 
3
4
  /*
4
5
  Unnesting parameters
@@ -248,19 +249,29 @@ const ga4ExportDateFilters = (config) => {
248
249
  * @param {Object} [config.preOperations] - Contains full refresh date range values.
249
250
  * @returns {string} - SQL condition string to filter the query by date range.
250
251
  */
251
- const finalDataFilter = (config) => {
252
+ const incrementalDateFilter = (config) => {
252
253
  const setDateRange = (start, end) => {
253
254
  return `(event_date >= ${start} and event_date <= ${end})`;
254
255
  };
255
256
 
257
+ // test mode
256
258
  if (config.test) {
257
- return setDateRange(config.testConfig.dateRangeStart, config.testConfig.dateRangeEnd);
259
+ const testStart = config?.testConfig?.dateRangeStart || baseConfig.testConfig.dateRangeStart;
260
+ const testEnd = config?.testConfig?.dateRangeEnd || baseConfig.testConfig.dateRangeEnd;
261
+
262
+ return setDateRange(testStart, testEnd);
258
263
  }
264
+
265
+ // incremental mode
259
266
  if (config.incremental) {
260
267
  return setDateRange(constants.DATE_RANGE_START_VARIABLE, constants.DATE_RANGE_END_VARIABLE);
261
268
  }
262
-
263
- return setDateRange(config.preOperations.dateRangeStartFullRefresh, config.preOperations.dateRangeEnd);
269
+
270
+ // full refresh mode
271
+ const fullRefreshStart = config?.preOperations?.dateRangeStartFullRefresh || baseConfig.preOperations.dateRangeStartFullRefresh;
272
+ const fullRefreshEnd = config?.preOperations?.dateRangeEnd || baseConfig.preOperations.dateRangeEnd;
273
+
274
+ return setDateRange(fullRefreshStart, fullRefreshEnd);
264
275
  };
265
276
 
266
277
  /*
@@ -552,8 +563,14 @@ Aggregation
552
563
  * // => SQL expression for the last user_id by event_timestamp.
553
564
  */
554
565
  const aggregateValue = (column, aggregateType, timestampColumn) => {
555
- if (typeof column === 'undefined' || typeof timestampColumn === 'undefined') {
556
- throw new Error("aggregateValue: 'column' and 'timestampColumn' are required parameters and must be defined.");
566
+ if (typeof column === 'undefined') {
567
+ throw new Error("aggregateValue: 'column' is a required parameter and must be defined.");
568
+ }
569
+ if (typeof aggregateType === 'undefined') {
570
+ throw new Error("aggregateValue: 'aggregateType' is a required parameter and must be defined.");
571
+ }
572
+ if ((aggregateType === 'first' || aggregateType === 'last') && typeof timestampColumn === 'undefined') {
573
+ throw new Error(`aggregateValue: 'timestampColumn' is required when aggregateType is '${aggregateType}'.`);
557
574
  }
558
575
 
559
576
  if (aggregateType === 'max') {
@@ -587,6 +604,17 @@ const aggregateValue = (column, aggregateType, timestampColumn) => {
587
604
  throw new Error(`aggregateValue: Unsupported aggregateType '${aggregateType}'. Supported values are 'max', 'min', 'first', 'last', and 'any'.`);
588
605
  };
589
606
 
607
+ // perform aggregations on an array of values
608
+ const aggregateValues = (values) => {
609
+ if (Array.isArray(values)) {
610
+ return values.map(value => {
611
+ const sqlExpression = aggregateValue(value.column, value.aggregateType, value.timestampColumn)
612
+ return `${sqlExpression}${value.alias ? ` as ${value.alias}` : ''}`;
613
+ }).join(',\n ');
614
+ }
615
+ throw new Error("aggregateValues: 'values' must be an array of objects with 'column', 'aggregateType', and 'timestampColumn' properties.");
616
+ };
617
+
590
618
  /*
591
619
  Ecommerce
592
620
  */
@@ -737,6 +765,7 @@ module.exports = {
737
765
  unnestEventParam,
738
766
  sessionId,
739
767
  aggregateValue,
768
+ aggregateValues,
740
769
  fixEcommerceStruct,
741
770
  isFinalData,
742
771
  ga4ExportDateFilter,
@@ -744,7 +773,7 @@ module.exports = {
744
773
  filterEventParams,
745
774
  aggregateSessionParams,
746
775
  excludeNullSessionParams,
747
- finalDataFilter,
776
+ incrementalDateFilter,
748
777
  extractPageDetails,
749
778
  extractUrlHostname,
750
779
  extractUrlPath,
package/index.js CHANGED
@@ -1,37 +1,17 @@
1
1
  const helpers = require('./helpers.js');
2
2
  const ga4EventsEnhanced = require('./tables/ga4EventsEnhanced.js');
3
3
  const preOperations = require('./preOperations.js');
4
- const { validateConfig } = require('./inputValidation.js');
4
+ const { validateBaseConfig, validateEnhancedEventsConfig } = require('./inputValidation.js');
5
+ const { mergeSQLConfigurations } = require('./utils.js');
6
+ const { baseConfig } = require('./defaultConfig.js');
5
7
 
6
8
  // export setPreOperations with default configuration for usage with downstream tables
7
9
  const setPreOperations = (config) => {
8
- if (!config || !config.self) {
9
- throw new Error('setPreOperations: config.self is required. Pass the table\'s "self()" reference in the config object.');
10
- }
11
- if (config.incremental === undefined || config.incremental === null) {
12
- throw new Error('setPreOperations: config.incremental is required. Pass a boolean indicating whether the table uses incremental mode.');
13
- }
10
+ // merge the input config with the defaults
11
+ const mergedConfig = mergeSQLConfigurations(baseConfig, config);
14
12
 
15
- /*
16
- Todo: consider improving the validateConfig function to cover this use case as well
17
- */
18
-
19
- const defaultConfig = {
20
- self: undefined,
21
- incremental: undefined,
22
- test: false,
23
- testConfig: {
24
- dateRangeStart: 'current_date()-1',
25
- dateRangeEnd: 'current_date()',
26
- },
27
- preOperations: {
28
- dateRangeStartFullRefresh: 'date(2000, 1, 1)',
29
- dateRangeEnd: 'current_date()',
30
- numberOfPreviousDaysToScan: 10,
31
- },
32
- };
33
-
34
- const mergedConfig = utils.mergeSQLConfigurations(defaultConfig, config);
13
+ // do input validation on the merged config
14
+ validateBaseConfig(mergedConfig);
35
15
 
36
16
  return preOperations.setPreOperations(mergedConfig);
37
17
  };
@@ -40,5 +20,6 @@ module.exports = {
40
20
  helpers,
41
21
  ga4EventsEnhanced,
42
22
  setPreOperations,
43
- validateConfig
23
+ validateBaseConfig,
24
+ validateEnhancedEventsConfig
44
25
  };
@@ -1,5 +1,86 @@
1
1
  const { isDataformTableReferenceObject } = require('./utils.js');
2
2
 
3
+ /**
4
+ * Validates the base configuration fields shared across all table types.
5
+ * These correspond to the fields defined in baseConfig (defaultConfig.js):
6
+ * self, incremental, test, testConfig, and preOperations.
7
+ *
8
+ * @param {Object} config - The merged configuration object to validate.
9
+ * @throws {Error} If any base configuration value is invalid or missing.
10
+ */
11
+ const validateBaseConfig = (config) => {
12
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
13
+ throw new Error(`config must be a non-null object. Received: ${JSON.stringify(config)}`);
14
+ }
15
+
16
+ // self - required, must be valid format
17
+ if (config.test !== true) {
18
+ if (typeof config.self !== 'string' || !config.self.trim() || !/^`[^`]+`$/.test(config.self.trim())) {
19
+ throw new Error(`config.self is required when config.test !== true and must be a non-empty string in format '\`project.dataset.table\`' (using the ref() function). Received: ${JSON.stringify(config.self)}`);
20
+ }
21
+ }
22
+
23
+ // incremental - required, must be boolean
24
+ if (typeof config.incremental !== 'boolean') {
25
+ throw new Error(`config.incremental must be a boolean. Received: ${JSON.stringify(config.incremental)}`);
26
+ }
27
+
28
+ // test - optional; when defined, must be a boolean
29
+ if (typeof config.test !== 'undefined' && typeof config.test !== 'boolean') {
30
+ throw new Error(`config.test must be a boolean when defined. Received: ${JSON.stringify(config.test)}`);
31
+ }
32
+
33
+ // testConfig - optional; when included, must be an object with optional dateRangeStart and dateRangeEnd
34
+ if (typeof config.testConfig !== 'undefined') {
35
+ if (!config.testConfig || typeof config.testConfig !== 'object' || Array.isArray(config.testConfig)) {
36
+ throw new Error(`config.testConfig must be an object when included. Received: ${JSON.stringify(config.testConfig)}`);
37
+ }
38
+ if (config.testConfig.dateRangeStart !== undefined && (typeof config.testConfig.dateRangeStart !== 'string' || !config.testConfig.dateRangeStart.trim())) {
39
+ throw new Error(`config.testConfig.dateRangeStart must be a non-empty string (SQL date expression) when provided. Received: ${JSON.stringify(config.testConfig.dateRangeStart)}`);
40
+ }
41
+ if (config.testConfig.dateRangeEnd !== undefined && (typeof config.testConfig.dateRangeEnd !== 'string' || !config.testConfig.dateRangeEnd.trim())) {
42
+ throw new Error(`config.testConfig.dateRangeEnd must be a non-empty string (SQL date expression) when provided. Received: ${JSON.stringify(config.testConfig.dateRangeEnd)}`);
43
+ }
44
+ }
45
+
46
+ // preOperations - required
47
+ if (config.preOperations === undefined) {
48
+ throw new Error("config.preOperations is required.");
49
+ }
50
+ if (!config.preOperations || typeof config.preOperations !== 'object' || Array.isArray(config.preOperations)) {
51
+ throw new Error(`config.preOperations must be an object. Received: ${JSON.stringify(config.preOperations)}`);
52
+ }
53
+ if (config.preOperations.numberOfPreviousDaysToScan === undefined) {
54
+ throw new Error("config.preOperations.numberOfPreviousDaysToScan is required.");
55
+ }
56
+ const v = config.preOperations.numberOfPreviousDaysToScan;
57
+ if (typeof v !== 'number' || isNaN(v) || !Number.isInteger(v) || v < 0) {
58
+ throw new Error(`config.preOperations.numberOfPreviousDaysToScan must be a non-negative integer. Received: ${JSON.stringify(v)}`);
59
+ }
60
+ if (config.preOperations.dateRangeStartFullRefresh === undefined || config.preOperations.dateRangeStartFullRefresh === null) {
61
+ throw new Error("config.preOperations.dateRangeStartFullRefresh is required.");
62
+ }
63
+ if (typeof config.preOperations.dateRangeStartFullRefresh !== 'string' || !config.preOperations.dateRangeStartFullRefresh.trim()) {
64
+ throw new Error(`config.preOperations.dateRangeStartFullRefresh must be a non-empty string (SQL date expression). Received: ${JSON.stringify(config.preOperations.dateRangeStartFullRefresh)}`);
65
+ }
66
+ if (config.preOperations.dateRangeEnd === undefined || config.preOperations.dateRangeEnd === null) {
67
+ throw new Error("config.preOperations.dateRangeEnd is required.");
68
+ }
69
+ if (typeof config.preOperations.dateRangeEnd !== 'string' || !config.preOperations.dateRangeEnd.trim()) {
70
+ throw new Error(`config.preOperations.dateRangeEnd must be a non-empty string (SQL date expression). Received: ${JSON.stringify(config.preOperations.dateRangeEnd)}`);
71
+ }
72
+ if (config.preOperations.incrementalStartOverride !== undefined && config.preOperations.incrementalStartOverride !== null && config.preOperations.incrementalStartOverride !== '') {
73
+ if (typeof config.preOperations.incrementalStartOverride !== 'string' || !config.preOperations.incrementalStartOverride.trim()) {
74
+ throw new Error(`config.preOperations.incrementalStartOverride must be a non-empty string when provided. Received: ${JSON.stringify(config.preOperations.incrementalStartOverride)}`);
75
+ }
76
+ }
77
+ if (config.preOperations.incrementalEndOverride !== undefined && config.preOperations.incrementalEndOverride !== null && config.preOperations.incrementalEndOverride !== '') {
78
+ if (typeof config.preOperations.incrementalEndOverride !== 'string' || !config.preOperations.incrementalEndOverride.trim()) {
79
+ throw new Error(`config.preOperations.incrementalEndOverride must be a non-empty string when provided. Received: ${JSON.stringify(config.preOperations.incrementalEndOverride)}`);
80
+ }
81
+ }
82
+ };
83
+
3
84
  /**
4
85
  * Validates a GA4 export fixer configuration object.
5
86
  * Validation is performed on mergedConfig (default values merged with user input).
@@ -9,12 +90,19 @@ const { isDataformTableReferenceObject } = require('./utils.js');
9
90
  * @param {Object} config - The merged configuration object to validate.
10
91
  * @throws {Error} If any configuration value is invalid or missing.
11
92
  */
12
- const validateConfig = (config) => {
93
+ const validateEnhancedEventsConfig = (config) => {
13
94
  try {
14
95
  if (!config || typeof config !== 'object' || Array.isArray(config)) {
15
96
  throw new Error(`config must be a non-null object. Received: ${JSON.stringify(config)}`);
16
97
  }
17
98
 
99
+ // base config fields (self, incremental, test, testConfig, preOperations)
100
+ validateBaseConfig(config);
101
+
102
+ /*
103
+ Rest of the validations are related to ga4_events_enhanced table specific fields
104
+ */
105
+
18
106
  // sourceTable - required; string or Dataform table reference
19
107
  if (config.sourceTable === undefined || config.sourceTable === null) {
20
108
  throw new Error("config.sourceTable is required. Provide a Dataform table reference (using the ref() function) or a string in format '`project.dataset.table`'.");
@@ -32,19 +120,6 @@ const validateConfig = (config) => {
32
120
  throw new Error(`config.sourceTable must be a Dataform table reference object or a string in format '\`project.dataset.table\`'. Received: ${JSON.stringify(config.sourceTable)}`);
33
121
  }
34
122
 
35
- // self - required when using Dataform; must be valid format
36
- // config.self is required when config.test === true and must be a non-empty string in format '\`project.dataset.table\`' (using the ref() function)
37
- if (config.test !== true) {
38
- if (typeof config.self !== 'string' || !config.self.trim() || !/^`[^`]+`$/.test(config.self.trim())) {
39
- throw new Error(`config.self is required when config.test === true and must be a non-empty string in format '\`project.dataset.table\`' (using the ref() function). Received: ${JSON.stringify(config.self)}`);
40
- }
41
- }
42
-
43
- // incremental - required when using Dataform; must be boolean
44
- if (typeof config.incremental !== 'boolean') {
45
- throw new Error(`config.incremental must be a boolean. Received: ${JSON.stringify(config.incremental)}`);
46
- }
47
-
48
123
  // schemaLock - optional; must be undefined or a string in "YYYYMMDD" format (e.g., "20260101")
49
124
  if (typeof config.schemaLock !== 'undefined') {
50
125
  if (typeof config.schemaLock !== 'string' || !/^\d{8}$/.test(config.schemaLock)) {
@@ -132,66 +207,11 @@ const validateConfig = (config) => {
132
207
  throw new Error(`config.dataIsFinal.detectionMethod must be 'DAY_THRESHOLD' when only intraday export is enabled (config.includedExportTypes.daily is false). A dayThreshold of 1 is recommended for intraday-only configurations. Received: ${JSON.stringify(config.dataIsFinal.detectionMethod)}`);
133
208
  }
134
209
 
135
- // test - optional; when defined, must be a boolean
136
- if (typeof config.test !== 'undefined' && typeof config.test !== 'boolean') {
137
- throw new Error(`config.test must be a boolean when defined. Received: ${JSON.stringify(config.test)}`);
138
- }
139
-
140
- // testConfig - optional; when included, must be an object with optional dateRangeStart and dateRangeEnd
141
- if (typeof config.testConfig !== 'undefined') {
142
- if (!config.testConfig || typeof config.testConfig !== 'object' || Array.isArray(config.testConfig)) {
143
- throw new Error(`config.testConfig must be an object when included. Received: ${JSON.stringify(config.testConfig)}`);
144
- }
145
- if (config.testConfig.dateRangeStart !== undefined && (typeof config.testConfig.dateRangeStart !== 'string' || !config.testConfig.dateRangeStart.trim())) {
146
- throw new Error(`config.testConfig.dateRangeStart must be a non-empty string (SQL date expression) when provided. Received: ${JSON.stringify(config.testConfig.dateRangeStart)}`);
147
- }
148
- if (config.testConfig.dateRangeEnd !== undefined && (typeof config.testConfig.dateRangeEnd !== 'string' || !config.testConfig.dateRangeEnd.trim())) {
149
- throw new Error(`config.testConfig.dateRangeEnd must be a non-empty string (SQL date expression) when provided. Received: ${JSON.stringify(config.testConfig.dateRangeEnd)}`);
150
- }
151
- }
152
-
153
210
  // bufferDays - required
154
211
  if (typeof config.bufferDays !== 'number' || !Number.isInteger(config.bufferDays) || config.bufferDays < 0) {
155
212
  throw new Error(`config.bufferDays must be a non-negative integer. Received: ${JSON.stringify(config.bufferDays)}`);
156
213
  }
157
214
 
158
- // preOperations - required
159
- if (config.preOperations === undefined) {
160
- throw new Error("config.preOperations is required.");
161
- }
162
- if (!config.preOperations || typeof config.preOperations !== 'object' || Array.isArray(config.preOperations)) {
163
- throw new Error(`config.preOperations must be an object. Received: ${JSON.stringify(config.preOperations)}`);
164
- }
165
- if (config.preOperations.numberOfPreviousDaysToScan === undefined) {
166
- throw new Error("config.preOperations.numberOfPreviousDaysToScan is required.");
167
- }
168
- const v = config.preOperations.numberOfPreviousDaysToScan;
169
- if (typeof v !== 'number' || isNaN(v) || !Number.isInteger(v) || v < 0) {
170
- throw new Error(`config.preOperations.numberOfPreviousDaysToScan must be a non-negative integer. Received: ${JSON.stringify(v)}`);
171
- }
172
- if (config.preOperations.dateRangeStartFullRefresh === undefined || config.preOperations.dateRangeStartFullRefresh === null) {
173
- throw new Error("config.preOperations.dateRangeStartFullRefresh is required.");
174
- }
175
- if (typeof config.preOperations.dateRangeStartFullRefresh !== 'string' || !config.preOperations.dateRangeStartFullRefresh.trim()) {
176
- throw new Error(`config.preOperations.dateRangeStartFullRefresh must be a non-empty string (SQL date expression). Received: ${JSON.stringify(config.preOperations.dateRangeStartFullRefresh)}`);
177
- }
178
- if (config.preOperations.dateRangeEnd === undefined || config.preOperations.dateRangeEnd === null) {
179
- throw new Error("config.preOperations.dateRangeEnd is required.");
180
- }
181
- if (typeof config.preOperations.dateRangeEnd !== 'string' || !config.preOperations.dateRangeEnd.trim()) {
182
- throw new Error(`config.preOperations.dateRangeEnd must be a non-empty string (SQL date expression). Received: ${JSON.stringify(config.preOperations.dateRangeEnd)}`);
183
- }
184
- if (config.preOperations.incrementalStartOverride !== undefined && config.preOperations.incrementalStartOverride !== null && config.preOperations.incrementalStartOverride !== '') {
185
- if (typeof config.preOperations.incrementalStartOverride !== 'string' || !config.preOperations.incrementalStartOverride.trim()) {
186
- throw new Error(`config.preOperations.incrementalStartOverride must be a non-empty string when provided. Received: ${JSON.stringify(config.preOperations.incrementalStartOverride)}`);
187
- }
188
- }
189
- if (config.preOperations.incrementalEndOverride !== undefined && config.preOperations.incrementalEndOverride !== null && config.preOperations.incrementalEndOverride !== '') {
190
- if (typeof config.preOperations.incrementalEndOverride !== 'string' || !config.preOperations.incrementalEndOverride.trim()) {
191
- throw new Error(`config.preOperations.incrementalEndOverride must be a non-empty string when provided. Received: ${JSON.stringify(config.preOperations.incrementalEndOverride)}`);
192
- }
193
- }
194
-
195
215
  // Array fields - all required
196
216
  const stringArrayKeys = ['defaultExcludedEventParams', 'excludedEventParams', 'sessionParams', 'defaultExcludedEvents', 'excludedEvents', 'excludedColumns'];
197
217
  for (const key of stringArrayKeys) {
@@ -242,5 +262,6 @@ const validateConfig = (config) => {
242
262
  };
243
263
 
244
264
  module.exports = {
245
- validateConfig
265
+ validateBaseConfig,
266
+ validateEnhancedEventsConfig
246
267
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ga4-export-fixer",
3
- "version": "0.1.6-dev.1",
3
+ "version": "0.1.6-dev.10",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -10,7 +10,9 @@
10
10
  "preOperations.js",
11
11
  "constants.js",
12
12
  "tables",
13
- "inputValidation.js"
13
+ "inputValidation.js",
14
+ "defaultConfig.js",
15
+ "config.js"
14
16
  ],
15
17
  "scripts": {
16
18
  "test": "node tests/ga4EventsEnhanced.test.js",
package/preOperations.js CHANGED
@@ -85,7 +85,7 @@ const getDateRangeStartIntraday = (config) => {
85
85
  date
86
86
  )
87
87
  select
88
- max(
88
+ min(
89
89
  if(
90
90
  intraday = true and daily = false,
91
91
  date,
@@ -3,71 +3,11 @@ const utils = require('../utils.js');
3
3
  const inputValidation = require('../inputValidation.js');
4
4
  const constants = require('../constants.js');
5
5
  const preOperations = require('../preOperations.js');
6
+ const { ga4EventsEnhancedConfig } = require('../defaultConfig.js'); // config defaults
6
7
 
7
8
  // default configuration for the GA4 Events Enhanced table
8
9
  const defaultConfig = {
9
- // required
10
- sourceTable: undefined,
11
- sourceTableType: 'GA4_EXPORT', // used with pre operations to detect if ga4 export specific pre operations are needed
12
- self: undefined,
13
- incremental: undefined,
14
- // optional but recommended
15
- schemaLock: undefined,
16
- // only used with js tables
17
- // dataformTableConfig: {},
18
- // optional
19
- includedExportTypes: {
20
- daily: true,
21
- intraday: true,
22
- fresh: false,
23
- },
24
- timezone: 'Etc/UTC',
25
- customTimestampParam: undefined,
26
- dataIsFinal: {
27
- detectionMethod: 'EXPORT_TYPE', // or 'DAY_THRESHOLD'
28
- dayThreshold: 4 // only used if detectionMethod is 'DAY_THRESHOLD'
29
- },
30
- test: false,
31
- testConfig: {
32
- dateRangeStart: 'current_date()-1',
33
- dateRangeEnd: 'current_date()',
34
- },
35
- // number of additional days to take in for taking into account sessions that overlap days
36
- bufferDays: 1,
37
- preOperations: {
38
- dateRangeStartFullRefresh: 'date(2000, 1, 1)',
39
- dateRangeEnd: 'current_date()',
40
- // incrementalStartOverride and incrementalEndOverride are used to override the date range start and end for incremental refresh
41
- // this is useful if you want to re-process only a specific date range
42
- incrementalStartOverride: undefined,
43
- incrementalEndOverride: undefined,
44
- numberOfPreviousDaysToScan: 10,
45
- },
46
- // these parameters are excluded by default because they've been made available in other columns
47
- defaultExcludedEventParams: [
48
- 'page_location',
49
- 'ga_session_id',
50
- //'custom_event_timestamp', // removed if customTimestampParam is used
51
- ],
52
- excludedEventParams: [],
53
- eventParamsToColumns: [
54
- //{name: 'page_location', type: 'string', columnName: 'page_location2'},
55
- ],
56
- sessionParams: [],
57
- defaultExcludedEvents: [],
58
- // session_start and first_visit are excluded via the excludedEvents array
59
- // this allows the user to include them if needed
60
- excludedEvents: [
61
- 'session_start',
62
- 'first_visit'
63
- ],
64
- defaultExcludedColumns: [
65
- 'event_dimensions', // legacy column, not needed
66
- 'traffic_source', // renamed to user_traffic_source
67
- 'session_id'
68
- ],
69
- // exclude these columns when extracting raw data from the export tables
70
- excludedColumns: [],
10
+ ...ga4EventsEnhancedConfig,
71
11
  };
72
12
 
73
13
  // List the columns in the order they should be in the final table
@@ -208,7 +148,7 @@ const generateEnhancedEventsSQL = (config) => {
208
148
  const mergedConfig = utils.mergeSQLConfigurations(defaultConfig, config);
209
149
 
210
150
  // validate the config and throw an error if it's invalid
211
- inputValidation.validateConfig(mergedConfig);
151
+ inputValidation.validateEnhancedEventsConfig(mergedConfig);
212
152
 
213
153
  if (!mergedConfig.sourceTable || typeof mergedConfig.sourceTable !== 'string' || mergedConfig.sourceTable.trim() === '') {
214
154
  throw new Error("generateEnhancedEventsSQL: 'sourceTable' is a required parameter in config and must be a non-empty string.");
@@ -342,7 +282,7 @@ ${excludedEventsSQL}`,
342
282
  condition: 'using(session_id)'
343
283
  }
344
284
  ],
345
- where: helpers.finalDataFilter(mergedConfig)
285
+ where: helpers.incrementalDateFilter(mergedConfig)
346
286
  };
347
287
 
348
288
  const steps = [