fg-devkit 1.17.3 → 1.19.2
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/.cursor/rules/core-coding-standards.mdc +24 -0
- package/README.md +25 -0
- package/configs/dependency-policy.vue-workspace.json +30 -30
- package/package.json +17 -14
- package/scaffold/vue/package.json +12 -12
- package/scripts/fg-devkit.mjs +154 -2
- package/scripts/piwik-common.mjs +653 -0
- package/scripts/piwik-dimensions.mjs +785 -0
- package/scripts/piwik-dimensions.test.mjs +400 -0
- package/scripts/piwik.mjs +502 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Maintainer tool for Piwik PRO custom dimensions.
|
|
4
|
+
*
|
|
5
|
+
* Reads the consumer's local slot map (src/analytics/dimensions.ts or
|
|
6
|
+
* src/util/piwikDimensions.ts) and compares it with the site in Piwik.
|
|
7
|
+
* Session and event dimensions are created one at a time, and only when the
|
|
8
|
+
* next free slot in that scope is the local slot. Product dimensions send the
|
|
9
|
+
* local slot (1–20). There is no delete: deactivating a dimension does not
|
|
10
|
+
* free its slot.
|
|
11
|
+
*
|
|
12
|
+
* pnpm exec fg-devkit piwik list [--local] [--env preprod] [--scope event]
|
|
13
|
+
* pnpm exec fg-devkit piwik diff
|
|
14
|
+
* pnpm exec fg-devkit piwik add [--yes] [--dry-run]
|
|
15
|
+
* pnpm exec fg-devkit piwik update [--yes] [--dry-run]
|
|
16
|
+
* pnpm exec fg-devkit piwik sync [--yes] [--dry-run]
|
|
17
|
+
* pnpm exec fg-devkit piwik pull [--env preprod] [--yes] [--dry-run]
|
|
18
|
+
* pnpm exec fg-devkit piwik compare [--from preprod] [--to production]
|
|
19
|
+
* pnpm exec fg-devkit piwik migrate [--from preprod] [--to production] [--yes] [--dry-run]
|
|
20
|
+
*
|
|
21
|
+
* Credentials: PIWIK_CLIENT_ID and PIWIK_CLIENT_SECRET (env, .env.local, or env.local).
|
|
22
|
+
* Site: --account / --website-id, piwik/.piwik.json, or src/config/env.ts.
|
|
23
|
+
* Optional piwik/.piwik.json names override the Piwik label for a local key.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import fs from 'node:fs'
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
PiwikDimensionError,
|
|
30
|
+
SCOPES,
|
|
31
|
+
diffDimensions,
|
|
32
|
+
diffEnvironments,
|
|
33
|
+
formatDimensionReport,
|
|
34
|
+
formatEnvironmentReport,
|
|
35
|
+
planCreates,
|
|
36
|
+
planEnvironmentMigration,
|
|
37
|
+
planLocalPull,
|
|
38
|
+
renderTable,
|
|
39
|
+
rewriteDimensionNumbers,
|
|
40
|
+
} from './piwik-dimensions.mjs'
|
|
41
|
+
import {
|
|
42
|
+
assertApiConfig,
|
|
43
|
+
createPiwikClient,
|
|
44
|
+
loadLocalDimensions,
|
|
45
|
+
resolveEnvironmentPair,
|
|
46
|
+
resolvePiwikConfig,
|
|
47
|
+
} from './piwik-common.mjs'
|
|
48
|
+
|
|
49
|
+
const COMMANDS = new Set(['list', 'diff', 'add', 'update', 'sync', 'pull', 'compare', 'migrate'])
|
|
50
|
+
|
|
51
|
+
function printHelp() {
|
|
52
|
+
console.log(`Usage:
|
|
53
|
+
pnpm exec fg-devkit piwik <command> [options]
|
|
54
|
+
|
|
55
|
+
Commands:
|
|
56
|
+
list List dimensions in Piwik. --local prints the repo map and skips the API.
|
|
57
|
+
diff Compare the local slot map with Piwik. Exits 1 when they differ.
|
|
58
|
+
add Create local dimensions that are missing in Piwik, in slot order.
|
|
59
|
+
update Rename Piwik dimensions whose name differs from the local key.
|
|
60
|
+
sync Copy the local map up to Piwik: add missing dimensions, then rename. Does not edit the repo.
|
|
61
|
+
pull Copy tracking ids from Piwik down into the local map for keys with the same name.
|
|
62
|
+
compare Compare two sites from src/config/env.ts. Exits 1 when they differ.
|
|
63
|
+
migrate Copy dimensions from --from onto --to. Creates one at a time and checks tracking_id.
|
|
64
|
+
|
|
65
|
+
Options:
|
|
66
|
+
--env <name> Environment object in src/config/env.ts (default: first non-production site, such as preprod)
|
|
67
|
+
--from <name> Source site for compare and migrate (default: first non-production site)
|
|
68
|
+
--to <name> Target site for compare and migrate (default: production when that site exists)
|
|
69
|
+
--account <name> Piwik account (the subdomain of {account}.piwik.pro)
|
|
70
|
+
--website-id <uuid> Site id. Defaults to piwikId / piwikProId for --env
|
|
71
|
+
--dimensions <file> Local map. Default: src/analytics/dimensions.ts or src/util/piwikDimensions.ts
|
|
72
|
+
--config <file> Optional piwik/.piwik.json (account, websiteId, environment, dimensionsFile, names)
|
|
73
|
+
--scope <event|session|product>
|
|
74
|
+
--client-id <id> Overrides PIWIK_CLIENT_ID
|
|
75
|
+
--client-secret <secret>
|
|
76
|
+
--yes Apply add, update, or sync. Without this, a TTY asks first and other runs only print the plan.
|
|
77
|
+
--dry-run Print the plan and do not call create or update
|
|
78
|
+
--local With list: print only the local map
|
|
79
|
+
|
|
80
|
+
The tracker sends dimension{slot}. Slot and tracking_id are read-only and assigned
|
|
81
|
+
by Piwik for session and event dimensions. A create stops the loop if the returned
|
|
82
|
+
slot is not the local slot.`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {import('./piwik-dimensions.mjs').LocalDimension[]} locals
|
|
87
|
+
*/
|
|
88
|
+
function printLocal(locals) {
|
|
89
|
+
for (const scope of SCOPES) {
|
|
90
|
+
const scoped = locals.filter((item) => item.scope === scope).sort((a, b) => a.slot - b.slot)
|
|
91
|
+
if (scoped.length === 0) continue
|
|
92
|
+
console.log(`\n${scope}`)
|
|
93
|
+
console.log(`${'slot'.padEnd(6)}key`)
|
|
94
|
+
for (const local of scoped) {
|
|
95
|
+
console.log(`${String(local.slot).padEnd(6)}${local.name}`)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const counts = SCOPES.map((scope) => `${locals.filter((item) => item.scope === scope).length} ${scope}`).join(', ')
|
|
99
|
+
console.log(`\n[piwik] ${locals.length} local dimensions (${counts})`)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {import('./piwik-dimensions.mjs').RemoteDimension[]} dimensions
|
|
104
|
+
* @param {Record<string, { used?: number, available?: number, left?: number }>} slots
|
|
105
|
+
*/
|
|
106
|
+
function printRemote(dimensions, slots) {
|
|
107
|
+
const slotLine = SCOPES.map((scope) => {
|
|
108
|
+
const stats = slots?.[scope]
|
|
109
|
+
if (!stats) return `${scope} ?`
|
|
110
|
+
return `${scope} ${stats.used ?? '?'}/${stats.available ?? '?'} (${stats.left ?? '?'} left)`
|
|
111
|
+
}).join(' ')
|
|
112
|
+
console.log(`[piwik] slots ${slotLine}`)
|
|
113
|
+
for (const scope of SCOPES) {
|
|
114
|
+
const scoped = dimensions.filter((item) => item.scope === scope).sort((a, b) => a.slot - b.slot)
|
|
115
|
+
if (scoped.length === 0) continue
|
|
116
|
+
console.log(`\n${scope}`)
|
|
117
|
+
const showDescription = scoped.some((remote) => remote.description.trim() !== '')
|
|
118
|
+
const headers = ['slot', 'tracking_id', 'state', 'name']
|
|
119
|
+
if (showDescription) headers.push('description')
|
|
120
|
+
console.log(
|
|
121
|
+
renderTable(
|
|
122
|
+
headers,
|
|
123
|
+
scoped.map((remote) => {
|
|
124
|
+
const cells = [String(remote.slot), String(remote.trackingId), remote.active ? 'active' : 'inactive', remote.name]
|
|
125
|
+
if (showDescription) cells.push(remote.description.trim())
|
|
126
|
+
return cells
|
|
127
|
+
}),
|
|
128
|
+
).join('\n'),
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
console.log(`\n[piwik] ${dimensions.length} dimensions in Piwik`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @param {string} message
|
|
136
|
+
* @param {{ yes: boolean, dryRun: boolean }} config
|
|
137
|
+
*/
|
|
138
|
+
async function confirmApply(message, config) {
|
|
139
|
+
if (config.dryRun) return false
|
|
140
|
+
if (config.yes) return true
|
|
141
|
+
if (!process.stdin.isTTY) {
|
|
142
|
+
console.log('[piwik] Not applied. Re-run with --yes to write to Piwik.')
|
|
143
|
+
return false
|
|
144
|
+
}
|
|
145
|
+
const { default: confirm } = await import('@inquirer/confirm')
|
|
146
|
+
return confirm({ message, default: false })
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param {ReturnType<typeof createPiwikClient>} client
|
|
151
|
+
* @param {ReturnType<typeof resolvePiwikConfig>} config
|
|
152
|
+
* @param {import('./piwik-dimensions.mjs').LocalDimension[]} creates
|
|
153
|
+
*/
|
|
154
|
+
async function applyCreates(client, config, creates) {
|
|
155
|
+
for (const local of creates) {
|
|
156
|
+
console.log(`[piwik] creating ${local.scope} ${local.name} dimension${local.slot}`)
|
|
157
|
+
const created =
|
|
158
|
+
local.scope === 'product'
|
|
159
|
+
? await client.createProductDimension({
|
|
160
|
+
websiteId: config.websiteId,
|
|
161
|
+
name: local.name,
|
|
162
|
+
slot: local.slot,
|
|
163
|
+
})
|
|
164
|
+
: await client.createDimension({
|
|
165
|
+
websiteId: config.websiteId,
|
|
166
|
+
name: local.name,
|
|
167
|
+
scope: local.scope,
|
|
168
|
+
})
|
|
169
|
+
const assignedId = local.scope === 'product' ? created.slot : created.trackingId
|
|
170
|
+
if (assignedId !== local.slot) {
|
|
171
|
+
throw new PiwikDimensionError(
|
|
172
|
+
`Piwik assigned ${created.scope} slot ${created.slot} (tracking_id ${created.trackingId}) to "${created.name}" (id ${created.id}), but the app sends dimension${local.slot}. Stopped before the next create. Deactivating the dimension does not free the slot.`,
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
console.log(`[piwik] created id ${created.id} ${created.scope} slot ${created.slot} tracking_id ${created.trackingId}`)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @param {ReturnType<typeof createPiwikClient>} client
|
|
181
|
+
* @param {string} websiteId
|
|
182
|
+
* @param {{ local: import('./piwik-dimensions.mjs').LocalDimension, remote: import('./piwik-dimensions.mjs').RemoteDimension }[]} updates
|
|
183
|
+
*/
|
|
184
|
+
async function applyUpdates(client, websiteId, updates) {
|
|
185
|
+
for (const { local, remote } of updates) {
|
|
186
|
+
const target = remote.websiteId ? remote : { ...remote, websiteId }
|
|
187
|
+
console.log(`[piwik] updating ${remote.scope} slot ${remote.slot} "${remote.name}" -> "${local.name}"`)
|
|
188
|
+
const updated =
|
|
189
|
+
remote.scope === 'product'
|
|
190
|
+
? await client.updateProductDimension(target, local.name)
|
|
191
|
+
: await client.updateDimension(target, local.name)
|
|
192
|
+
console.log(`[piwik] updated id ${updated.id} name "${updated.name}"`)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function main() {
|
|
197
|
+
const argv = process.argv.slice(2)
|
|
198
|
+
if (argv.includes('--help') || argv.includes('-h') || argv.length === 0) {
|
|
199
|
+
printHelp()
|
|
200
|
+
return argv.length === 0 ? 1 : 0
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const config = resolvePiwikConfig(argv)
|
|
204
|
+
if (!COMMANDS.has(config.command)) {
|
|
205
|
+
console.error(`[piwik] Unknown command: ${config.command}`)
|
|
206
|
+
printHelp()
|
|
207
|
+
return 1
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (config.command === 'list' && config.localOnly) {
|
|
211
|
+
printLocal(loadLocalDimensions(config))
|
|
212
|
+
return 0
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (config.command === 'compare' || config.command === 'migrate') {
|
|
216
|
+
return runEnvironmentCommand(config)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (config.command === 'pull') {
|
|
220
|
+
return runPull(config)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
assertApiConfig(config)
|
|
224
|
+
console.log(
|
|
225
|
+
`[piwik] account=${config.account} website=${config.websiteId} env=${config.envName}${config.siteSource ? ` (${config.siteSource})` : ''}`,
|
|
226
|
+
)
|
|
227
|
+
if (config.dimensionFiles.length > 0 && config.command !== 'list') {
|
|
228
|
+
console.log(`[piwik] local ${config.dimensionFiles.map((file) => file.replace(`${config.cwd}/`, '')).join(', ')}`)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const client = createPiwikClient({
|
|
232
|
+
account: config.account,
|
|
233
|
+
clientId: config.clientId,
|
|
234
|
+
clientSecret: config.clientSecret,
|
|
235
|
+
})
|
|
236
|
+
const remoteResult = await client.listDimensions(config.websiteId)
|
|
237
|
+
const remotes = config.scope
|
|
238
|
+
? remoteResult.dimensions.filter((item) => item.scope === config.scope)
|
|
239
|
+
: remoteResult.dimensions
|
|
240
|
+
for (const remote of remotes) {
|
|
241
|
+
if (!remote.websiteId) remote.websiteId = config.websiteId
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (config.command === 'list') {
|
|
245
|
+
printRemote(remotes, remoteResult.slots)
|
|
246
|
+
return 0
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const locals = loadLocalDimensions(config)
|
|
250
|
+
const diff = diffDimensions(locals, remotes)
|
|
251
|
+
const plan = planCreates(diff.missing, remotes)
|
|
252
|
+
console.log(formatDimensionReport(diff, plan))
|
|
253
|
+
|
|
254
|
+
const wantsCreate = config.command === 'add' || config.command === 'sync'
|
|
255
|
+
const wantsUpdate = config.command === 'update' || config.command === 'sync'
|
|
256
|
+
const drifted = diff.missing.length > 0 || diff.remoteOnly.length > 0
|
|
257
|
+
|
|
258
|
+
if (config.command === 'diff') return drifted ? 1 : 0
|
|
259
|
+
|
|
260
|
+
const createCount = wantsCreate ? plan.creates.length : 0
|
|
261
|
+
const updateCount = wantsUpdate ? diff.nameMismatch.length : 0
|
|
262
|
+
if (createCount === 0 && updateCount === 0) {
|
|
263
|
+
console.log('[piwik] Nothing to write.')
|
|
264
|
+
return plan.blocked.length > 0 && wantsCreate ? 1 : 0
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (config.dryRun) {
|
|
268
|
+
console.log('[piwik] Dry run. No dimensions were created or updated.')
|
|
269
|
+
return plan.blocked.length > 0 && wantsCreate ? 1 : 0
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const apply = await confirmApply(
|
|
273
|
+
`Copy ${createCount} new and ${updateCount} renamed dimension(s) from the local map up to ${config.envName || config.account}?`,
|
|
274
|
+
config,
|
|
275
|
+
)
|
|
276
|
+
if (!apply) return 0
|
|
277
|
+
|
|
278
|
+
/** @type {unknown} */
|
|
279
|
+
let writeError = null
|
|
280
|
+
try {
|
|
281
|
+
if (wantsCreate) await applyCreates(client, config, plan.creates)
|
|
282
|
+
if (wantsUpdate) await applyUpdates(client, config.websiteId, diff.nameMismatch)
|
|
283
|
+
} catch (error) {
|
|
284
|
+
writeError = error
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
console.log('\n[piwik] Piwik after the update:')
|
|
288
|
+
const refreshed = await listSiteDimensions(client, config.websiteId, config.scope)
|
|
289
|
+
const nextDiff = diffDimensions(locals, refreshed)
|
|
290
|
+
const nextPlan = planCreates(nextDiff.missing, refreshed)
|
|
291
|
+
console.log(formatDimensionReport(nextDiff, nextPlan))
|
|
292
|
+
if (writeError) throw writeError
|
|
293
|
+
|
|
294
|
+
const stillMissing = wantsCreate && (nextPlan.creates.length > 0 || nextPlan.blocked.length > 0)
|
|
295
|
+
const stillRenamed = wantsUpdate && nextDiff.nameMismatch.length > 0
|
|
296
|
+
return stillMissing || stillRenamed ? 1 : 0
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* @param {ReturnType<typeof createPiwikClient>} client
|
|
301
|
+
* @param {string} websiteId
|
|
302
|
+
* @param {string} [scope]
|
|
303
|
+
*/
|
|
304
|
+
/**
|
|
305
|
+
* Copy tracking ids from Piwik down into the local dimension map.
|
|
306
|
+
* @param {ReturnType<typeof resolvePiwikConfig>} config
|
|
307
|
+
*/
|
|
308
|
+
async function runPull(config) {
|
|
309
|
+
assertApiConfig(config)
|
|
310
|
+
console.log(
|
|
311
|
+
`[piwik] account=${config.account} website=${config.websiteId} env=${config.envName}${config.siteSource ? ` (${config.siteSource})` : ''}`,
|
|
312
|
+
)
|
|
313
|
+
console.log('[piwik] Pull copies tracking ids from Piwik down into the local map. Keys match on name.')
|
|
314
|
+
const locals = loadLocalDimensions(config)
|
|
315
|
+
console.log(`[piwik] local ${config.dimensionFiles.map((file) => file.replace(`${config.cwd}/`, '')).join(', ')}`)
|
|
316
|
+
const client = createPiwikClient({
|
|
317
|
+
account: config.account,
|
|
318
|
+
clientId: config.clientId,
|
|
319
|
+
clientSecret: config.clientSecret,
|
|
320
|
+
})
|
|
321
|
+
const remotes = await listSiteDimensions(client, config.websiteId, config.scope)
|
|
322
|
+
const plan = planLocalPull(locals, remotes)
|
|
323
|
+
console.log(
|
|
324
|
+
`[piwik] ${plan.inSync.length} in sync, ${plan.updates.length} to update in the repo, ${plan.missingInPiwik.length} missing in Piwik, ${plan.remoteOnly.length} only in Piwik`,
|
|
325
|
+
)
|
|
326
|
+
if (plan.updates.length > 0) {
|
|
327
|
+
console.log(
|
|
328
|
+
renderTable(
|
|
329
|
+
['scope', 'key', 'in repo', 'tracking_id in Piwik'],
|
|
330
|
+
plan.updates.map((item) => [
|
|
331
|
+
item.local.scope,
|
|
332
|
+
item.local.key,
|
|
333
|
+
String(item.local.slot),
|
|
334
|
+
String(item.next),
|
|
335
|
+
]),
|
|
336
|
+
).join('\n'),
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
if (plan.updates.length === 0) {
|
|
340
|
+
console.log('[piwik] Nothing to write.')
|
|
341
|
+
return 0
|
|
342
|
+
}
|
|
343
|
+
if (config.dryRun) {
|
|
344
|
+
console.log('[piwik] Dry run. The local dimension map was not changed.')
|
|
345
|
+
return 0
|
|
346
|
+
}
|
|
347
|
+
const apply = await confirmApply(
|
|
348
|
+
`Write ${plan.updates.length} tracking id(s) from Piwik into the local dimension map?`,
|
|
349
|
+
config,
|
|
350
|
+
)
|
|
351
|
+
if (!apply) return 0
|
|
352
|
+
|
|
353
|
+
/** @type {Map<string, { key: string, from: number, to: number }[]>} */
|
|
354
|
+
const byFile = new Map()
|
|
355
|
+
for (const item of plan.updates) {
|
|
356
|
+
const list = byFile.get(item.local.file) ?? []
|
|
357
|
+
list.push({ key: item.local.key, from: item.local.slot, to: item.next })
|
|
358
|
+
byFile.set(item.local.file, list)
|
|
359
|
+
}
|
|
360
|
+
for (const [file, updates] of byFile) {
|
|
361
|
+
const absolute = file.startsWith('/') ? file : `${config.cwd}/${file}`
|
|
362
|
+
const source = fs.readFileSync(absolute, 'utf8')
|
|
363
|
+
fs.writeFileSync(absolute, rewriteDimensionNumbers(source, updates))
|
|
364
|
+
console.log(`[piwik] updated ${file.replace(`${config.cwd}/`, '')}`)
|
|
365
|
+
}
|
|
366
|
+
return 0
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function listSiteDimensions(client, websiteId, scope) {
|
|
370
|
+
const result = await client.listDimensions(websiteId)
|
|
371
|
+
const dimensions = scope ? result.dimensions.filter((item) => item.scope === scope) : result.dimensions
|
|
372
|
+
for (const remote of dimensions) {
|
|
373
|
+
if (!remote.websiteId) remote.websiteId = websiteId
|
|
374
|
+
}
|
|
375
|
+
return dimensions
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* @param {ReturnType<typeof resolvePiwikConfig>} config
|
|
380
|
+
*/
|
|
381
|
+
async function runEnvironmentCommand(config) {
|
|
382
|
+
const { from, to } = resolveEnvironmentPair(config.cwd, config.fromEnv, config.toEnv)
|
|
383
|
+
assertApiConfig({ ...config, account: from.account, websiteId: from.websiteId })
|
|
384
|
+
if (from.account !== to.account) {
|
|
385
|
+
throw new PiwikDimensionError(
|
|
386
|
+
`${from.environment} uses account ${from.account} and ${to.environment} uses ${to.account}. Compare and migrate need the same Piwik account.`,
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
console.log(`[piwik] from=${from.environment} website=${from.websiteId}`)
|
|
391
|
+
console.log(`[piwik] to=${to.environment} website=${to.websiteId}`)
|
|
392
|
+
|
|
393
|
+
const client = createPiwikClient({
|
|
394
|
+
account: from.account,
|
|
395
|
+
clientId: config.clientId,
|
|
396
|
+
clientSecret: config.clientSecret,
|
|
397
|
+
})
|
|
398
|
+
const source = await listSiteDimensions(client, from.websiteId, config.scope)
|
|
399
|
+
const target = await listSiteDimensions(client, to.websiteId, config.scope)
|
|
400
|
+
const diff = diffEnvironments(source, target)
|
|
401
|
+
const plan = planEnvironmentMigration(diff, target)
|
|
402
|
+
console.log(formatEnvironmentReport(diff, plan, from.environment, to.environment))
|
|
403
|
+
|
|
404
|
+
const drifted = diff.changed.length > 0 || diff.missing.length > 0 || diff.targetOnly.length > 0
|
|
405
|
+
if (config.command === 'compare') return drifted ? 1 : 0
|
|
406
|
+
|
|
407
|
+
if (diff.targetOnly.length > 0) {
|
|
408
|
+
console.log(`[piwik] ${diff.targetOnly.length} dimension(s) exist only in ${to.environment} and are left as they are.`)
|
|
409
|
+
}
|
|
410
|
+
if (plan.creates.length === 0 && diff.changed.length === 0) {
|
|
411
|
+
console.log('[piwik] Nothing to write.')
|
|
412
|
+
return plan.blocked.length > 0 ? 1 : 0
|
|
413
|
+
}
|
|
414
|
+
if (config.dryRun) {
|
|
415
|
+
console.log('[piwik] Dry run. No dimensions were created or updated.')
|
|
416
|
+
return plan.blocked.length > 0 ? 1 : 0
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const apply = await confirmApply(
|
|
420
|
+
`Copy ${plan.creates.length} new and ${diff.changed.length} updated dimension(s) from ${from.environment} to ${to.environment}?`,
|
|
421
|
+
config,
|
|
422
|
+
)
|
|
423
|
+
if (!apply) return 0
|
|
424
|
+
|
|
425
|
+
/** @type {unknown} */
|
|
426
|
+
let writeError = null
|
|
427
|
+
try {
|
|
428
|
+
for (const { source: origin, target: current } of diff.changed) {
|
|
429
|
+
console.log(
|
|
430
|
+
`[piwik] updating ${current.scope} tracking_id ${current.trackingId} "${current.name}" -> "${origin.name}"`,
|
|
431
|
+
)
|
|
432
|
+
const updated =
|
|
433
|
+
current.scope === 'product'
|
|
434
|
+
? await client.updateProductDimensionState(current, { name: origin.name, description: origin.description })
|
|
435
|
+
: await client.updateDimensionState(current, {
|
|
436
|
+
name: origin.name,
|
|
437
|
+
description: origin.description,
|
|
438
|
+
active: origin.active,
|
|
439
|
+
})
|
|
440
|
+
console.log(`[piwik] updated id ${updated.id} name "${updated.name}"`)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
for (const local of plan.creates) {
|
|
444
|
+
const origin = source.find(
|
|
445
|
+
(item) =>
|
|
446
|
+
item.scope === local.scope && (item.scope === 'product' ? item.slot === local.slot : item.trackingId === local.slot),
|
|
447
|
+
)
|
|
448
|
+
if (!origin) {
|
|
449
|
+
throw new PiwikDimensionError(`Missing source dimension for ${local.scope} ${local.slot}.`)
|
|
450
|
+
}
|
|
451
|
+
console.log(`[piwik] creating ${origin.scope} ${origin.name} tracking_id ${origin.trackingId} on ${to.environment}`)
|
|
452
|
+
const created =
|
|
453
|
+
origin.scope === 'product'
|
|
454
|
+
? await client.createProductDimension({
|
|
455
|
+
websiteId: to.websiteId,
|
|
456
|
+
name: origin.name,
|
|
457
|
+
slot: origin.slot,
|
|
458
|
+
description: origin.description,
|
|
459
|
+
})
|
|
460
|
+
: await client.createDimension({
|
|
461
|
+
websiteId: to.websiteId,
|
|
462
|
+
name: origin.name,
|
|
463
|
+
scope: origin.scope,
|
|
464
|
+
description: origin.description,
|
|
465
|
+
active: origin.active,
|
|
466
|
+
})
|
|
467
|
+
const assignedId = origin.scope === 'product' ? created.slot : created.trackingId
|
|
468
|
+
const expectedId = origin.scope === 'product' ? origin.slot : origin.trackingId
|
|
469
|
+
if (assignedId !== expectedId) {
|
|
470
|
+
throw new PiwikDimensionError(
|
|
471
|
+
`Piwik assigned ${created.scope} slot ${created.slot} (tracking_id ${created.trackingId}) to "${created.name}" on ${to.environment}, but ${from.environment} uses tracking_id ${expectedId}. Stopped before the next create.`,
|
|
472
|
+
)
|
|
473
|
+
}
|
|
474
|
+
console.log(`[piwik] created id ${created.id} ${created.scope} slot ${created.slot} tracking_id ${created.trackingId}`)
|
|
475
|
+
}
|
|
476
|
+
} catch (error) {
|
|
477
|
+
writeError = error
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
console.log('\n[piwik] Piwik after the update:')
|
|
481
|
+
const refreshedSource = await listSiteDimensions(client, from.websiteId, config.scope)
|
|
482
|
+
const refreshedTarget = await listSiteDimensions(client, to.websiteId, config.scope)
|
|
483
|
+
const nextDiff = diffEnvironments(refreshedSource, refreshedTarget)
|
|
484
|
+
const nextPlan = planEnvironmentMigration(nextDiff, refreshedTarget)
|
|
485
|
+
console.log(formatEnvironmentReport(nextDiff, nextPlan, from.environment, to.environment))
|
|
486
|
+
if (writeError) throw writeError
|
|
487
|
+
|
|
488
|
+
return nextPlan.creates.length > 0 || nextPlan.blocked.length > 0 || nextDiff.changed.length > 0 ? 1 : 0
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
main()
|
|
492
|
+
.then((code) => {
|
|
493
|
+
process.exitCode = code ?? 0
|
|
494
|
+
})
|
|
495
|
+
.catch((error) => {
|
|
496
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
497
|
+
console.error(`[piwik] ${message}`)
|
|
498
|
+
if (!(error instanceof PiwikDimensionError) && error instanceof Error && error.stack) {
|
|
499
|
+
console.error(error.stack)
|
|
500
|
+
}
|
|
501
|
+
process.exitCode = 1
|
|
502
|
+
})
|