@mindful-web/marko-web-identity-x 1.79.6 → 1.79.8
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/CLAUDE.md +78 -1
- package/components/access.marko.js +2 -2
- package/components/comment-stream.marko.js +2 -2
- package/components/context.marko.js +2 -2
- package/components/form-access.marko.js +2 -2
- package/components/form-authenticate.marko.js +2 -2
- package/components/form-change-email.marko.js +2 -2
- package/components/form-login.marko.js +2 -2
- package/components/form-logout.marko.js +2 -2
- package/components/form-profile.marko.js +2 -2
- package/components/form-register.marko.js +2 -2
- package/components/google-init.marko +160 -30
- package/components/google-init.marko.js +163 -32
- package/components/google-sign-in-button.marko.js +2 -2
- package/components/identify.marko.js +2 -2
- package/components/non-auth-identify.marko.js +2 -2
- package/components/subscribe.marko.js +2 -2
- package/config.js +21 -4
- package/package.json +2 -2
- package/routes/google.js +109 -6
- package/service.js +7 -2
- package/utils/add-custom-attributes.js +50 -0
package/CLAUDE.md
CHANGED
|
@@ -105,7 +105,55 @@ conflict-guarded; non-fatal via `config.get('onHookError')`) → `impersonateApp
|
|
|
105
105
|
`onAuthenticationSuccess`, same order/hooks as email-link so Omeda rapid-identify + subscription
|
|
106
106
|
sync run identically. `profile-gate` computes `requiresUserInput` by checking required fields are
|
|
107
107
|
non-empty (deliberately not `forceProfileReVerification`). Responds `{ ok, requiresUserInput,
|
|
108
|
-
redirectTo, user, entity, … }`; the client redirects to `/user/profile` when
|
|
108
|
+
redirectTo, user, entity, additionalEventData, … }`; the client redirects to `/user/profile` when
|
|
109
|
+
input is required.
|
|
110
|
+
|
|
111
|
+
**`additionalEventData` and the auto-signup union.** Tenant Omeda formatters *assign*
|
|
112
|
+
`additionalEventData.autoSignups` in place, and that object is the only channel by which the browser
|
|
113
|
+
learns a subscription was created (see analytics below). Where they assign it varies, and three
|
|
114
|
+
repos do it twice:
|
|
115
|
+
|
|
116
|
+
| Writes `autoSignups` in | Repos |
|
|
117
|
+
| --- | --- |
|
|
118
|
+
| `onLoginLinkSentFormatter` only | ab-media, allured, encore360, pmmi |
|
|
119
|
+
| `onAuthenticationSuccessFormatter` only | fusable, cox-matthews |
|
|
120
|
+
| **Both** | industrial (both pkgs), ironmarkets, watt |
|
|
121
|
+
|
|
122
|
+
So the route gives **each hook its own object** and merges the two afterwards, taking the union of
|
|
123
|
+
`autoSignups` deduped by `productId`. Sharing one reference (the obvious move, and what
|
|
124
|
+
`routes/authenticate.js` does — it only ever calls one hook) would let the second hook's assignment
|
|
125
|
+
clobber the first, silently dropping a whole set of subscribe events on the both-hook repos. In the
|
|
126
|
+
email-link flow those hooks fire on **separate requests** (login vs authenticate) and each reports
|
|
127
|
+
its own set, so the union is what preserves existing behaviour. Do **not** "simplify" this back to a
|
|
128
|
+
shared object.
|
|
129
|
+
|
|
130
|
+
`requiresUserInput` goes to `onAuthenticationSuccess` **only**: the onLoginLinkSent formatters read a
|
|
131
|
+
truthy value as "skip the Omeda subscription lookup" and then opt the user into every configured
|
|
132
|
+
deployment type without deduping, firing a phantom auto-signup for an existing Omeda customer who
|
|
133
|
+
merely lacks an idx required field. The email-link flow never exposes it there either —
|
|
134
|
+
`routes/login.js` 400s before any hook runs.
|
|
135
|
+
|
|
136
|
+
`idxAutoSignupCount` rides on the transaction attributes so "sign-in worked, Omeda updated, GA4
|
|
137
|
+
recorded nothing" — the exact shape of the July 2026 bug — is a visible number rather than something
|
|
138
|
+
a stakeholder finds in a report weeks later.
|
|
139
|
+
|
|
140
|
+
**Analytics parity (`components/google-init.marko`).** The Vue flows get `dataLayer` + p1events for
|
|
141
|
+
free from the EventBus bridge in `browser/index.js`; the One-Tap callback is plain inline JS on a
|
|
142
|
+
`<script>` tag, so it has to emit the same events by hand. Three things it must keep in step with
|
|
143
|
+
`browser/authenticate.vue` + `browser/mixins/*`:
|
|
144
|
+
- **`identity-x-authenticated`** — payload mirrors `global-event-emitter.js`, which sets
|
|
145
|
+
`actionSource`, `loginSource` **and** `source` to the same value. GTM variables in the wild read
|
|
146
|
+
whichever one they were written against; emitting only `actionSource` silently breaks the rest.
|
|
147
|
+
- **`identity-x-auto-signup`** — one event per `additionalEventData.autoSignups` entry, shape
|
|
148
|
+
`{ entity, ...autoSignup, event }` per `global-auto-signup-event-emitter.js`. **This — not
|
|
149
|
+
`identity-x-authenticated` — is what the sites' GA4 "subscribe" conversion triggers on.** The
|
|
150
|
+
Omeda opt-in happens server-side either way, so a missing event here looks like "One Tap doesn't
|
|
151
|
+
subscribe anyone" when the subscription exists and only the tracking is gone. Also mirrored to
|
|
152
|
+
p1events as `Subscription / Subscribe`.
|
|
153
|
+
- **Navigation is deferred ~300ms** after the pushes. The email-link flow navigates in the same tick
|
|
154
|
+
but does so from a fully-loaded `/user/authenticate`; One Tap can resolve far earlier in a page's
|
|
155
|
+
life, before GTM has dispatched. `window.dataLayer` is also **assigned, not guarded** (`= … || []`)
|
|
156
|
+
so a sign-in that beats a deferred container still queues its events instead of dropping them.
|
|
109
157
|
|
|
110
158
|
**Tags** (`components/marko.json`, config read from `req.identityX.config`):
|
|
111
159
|
- `<marko-web-identity-x-google-init>` — loads the GSI client + defines `window.handleGoogleOneTapCredential`;
|
|
@@ -119,6 +167,35 @@ login/authenticate pages get the button with **no per-site edit**. For the site-
|
|
|
119
167
|
document layout, gated on `!req.identityX.token`. The button tag can also be placed in a content
|
|
120
168
|
meter / gate.
|
|
121
169
|
|
|
170
|
+
**Telemetry.** Two seams. Neither needs site configuration:
|
|
171
|
+
- `config.addRequestAttributes` — called with a flat object as the route progresses
|
|
172
|
+
(`idxGoogleStep`, `idxUserId`, `idxCreatedNewUser`, `idxRequiresUserInput`, `idxGoogleSurface`,
|
|
173
|
+
`idxGoogleMissingFieldsBehavior`, `idxAutoSignupCount`). **Defaults to `utils/add-custom-attributes.js`**, which tags
|
|
174
|
+
the current New Relic transaction, so the attributes ride on the `Transaction` *and* any
|
|
175
|
+
`TransactionError` it produces — a 500 says which step failed instead of being a bare GraphQL
|
|
176
|
+
message. That default resolves the agent **only if the app already loaded it** (checks
|
|
177
|
+
`require.cache`, never requires it cold — requiring `newrelic` would *start* an agent on sites
|
|
178
|
+
that don't use it), so it arrives fleet-wide on a dep bump and is inert everywhere else. Pass a
|
|
179
|
+
function to override, or a no-op to disable. Deliberately **not** `noticeError`: that records a
|
|
180
|
+
second error next to the one the error handler already captures and doubles the counts.
|
|
181
|
+
- `config.onHookError` — non-fatal errors, now called as `(error, customAttributes)` to match
|
|
182
|
+
`newrelic.noticeError`. The Google `sub` linkage failures report through it with their step and
|
|
183
|
+
user id, which keeps them distinguishable from fatal errors on the same route (they arrive on a
|
|
184
|
+
200).
|
|
185
|
+
|
|
186
|
+
Client-side, `fail()` fires a p1events `Identity / Authenticate Failed` event mirroring the success
|
|
187
|
+
event. There is no New Relic Browser agent on the monorail sites, so without it a client-side
|
|
188
|
+
failure reaches no backend at all. The originating surface (`button` | `prompt`, from GIS
|
|
189
|
+
`select_by`) is posted to the route as `body.surface` — both surfaces otherwise share a route, a
|
|
190
|
+
`loginSource` and an impersonation method, so nothing server-side could tell them apart.
|
|
191
|
+
|
|
192
|
+
**Failure handling.** All client-side failures funnel through `fail()` in `google-init.marko`: it
|
|
193
|
+
always `console.error`s, writes to `#google-sign-in-error` when that element is on the page (it is
|
|
194
|
+
rendered by `google-sign-in-button.marko`, so it exists only where the button does), and otherwise
|
|
195
|
+
— the site-wide floating prompt case — redirects to the login endpoint with `?redirectTo=`. Never
|
|
196
|
+
let a failure be a silent no-op: GIS closes the prompt on its own, so the user reads "nothing
|
|
197
|
+
happened" as success and only discovers they're anonymous on the next page load.
|
|
198
|
+
|
|
122
199
|
**CSS.** `.google-sign-in` styles ship in `marko-web-theme-monorail/scss/components/_identity-x.scss`
|
|
123
200
|
(with the rest of the auth CSS) — no separate import.
|
|
124
201
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.8/components/access.marko",
|
|
6
6
|
marko_component = require("./access.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
module_objectPath_module = require("@mindful-web/object-path"),
|
|
@@ -53,7 +53,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
53
53
|
}, marko_component);
|
|
54
54
|
|
|
55
55
|
marko_template.meta = {
|
|
56
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
56
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.8/components/access.marko",
|
|
57
57
|
component: "./access.marko",
|
|
58
58
|
tags: [
|
|
59
59
|
"@mindful-web/marko-core/components/resolve.marko"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.8/components/comment-stream.marko",
|
|
6
6
|
marko_component = require("./comment-stream.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
module_objectPath_module = require("@mindful-web/object-path"),
|
|
@@ -91,7 +91,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
91
91
|
}, marko_component);
|
|
92
92
|
|
|
93
93
|
marko_template.meta = {
|
|
94
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
94
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.8/components/comment-stream.marko",
|
|
95
95
|
component: "./comment-stream.marko",
|
|
96
96
|
tags: [
|
|
97
97
|
"@mindful-web/marko-web/components/browser-component.marko",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.8/components/context.marko",
|
|
6
6
|
marko_component = require("./context.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
marko_dynamicTag = require("marko/dist/runtime/helpers/dynamic-tag"),
|
|
@@ -49,7 +49,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
49
49
|
}, marko_component);
|
|
50
50
|
|
|
51
51
|
marko_template.meta = {
|
|
52
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
52
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.8/components/context.marko",
|
|
53
53
|
component: "./context.marko",
|
|
54
54
|
tags: [
|
|
55
55
|
"@mindful-web/marko-core/components/resolve.marko"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.8/components/form-access.marko",
|
|
6
6
|
marko_component = require("./form-access.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
module_defaultValue = require("@mindful-web/marko-core/utils/default-value"),
|
|
@@ -179,7 +179,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
179
179
|
}, marko_component);
|
|
180
180
|
|
|
181
181
|
marko_template.meta = {
|
|
182
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
182
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.8/components/form-access.marko",
|
|
183
183
|
component: "./form-access.marko",
|
|
184
184
|
tags: [
|
|
185
185
|
"@mindful-web/marko-web/components/browser-component.marko",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.8/components/form-authenticate.marko",
|
|
6
6
|
marko_component = require("./form-authenticate.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
module_objectPath_module = require("@mindful-web/object-path"),
|
|
@@ -81,7 +81,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
81
81
|
}, marko_component);
|
|
82
82
|
|
|
83
83
|
marko_template.meta = {
|
|
84
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
84
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.8/components/form-authenticate.marko",
|
|
85
85
|
component: "./form-authenticate.marko",
|
|
86
86
|
tags: [
|
|
87
87
|
"@mindful-web/marko-web/components/browser-component.marko",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.8/components/form-change-email.marko",
|
|
6
6
|
marko_component = require("./form-change-email.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
module_objectPath_module = require("@mindful-web/object-path"),
|
|
@@ -59,7 +59,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
59
59
|
}, marko_component);
|
|
60
60
|
|
|
61
61
|
marko_template.meta = {
|
|
62
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
62
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.8/components/form-change-email.marko",
|
|
63
63
|
component: "./form-change-email.marko",
|
|
64
64
|
tags: [
|
|
65
65
|
"@mindful-web/marko-web/components/browser-component.marko",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.8/components/form-login.marko",
|
|
6
6
|
marko_component = require("./form-login.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
module_objectPath_module = require("@mindful-web/object-path"),
|
|
@@ -92,7 +92,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
92
92
|
}, marko_component);
|
|
93
93
|
|
|
94
94
|
marko_template.meta = {
|
|
95
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
95
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.8/components/form-login.marko",
|
|
96
96
|
component: "./form-login.marko",
|
|
97
97
|
tags: [
|
|
98
98
|
"@mindful-web/marko-web/components/browser-component.marko",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.8/components/form-logout.marko",
|
|
6
6
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
7
7
|
module_defaultValue = require("@mindful-web/marko-core/utils/default-value"),
|
|
8
8
|
defaultValue = module_defaultValue.default || module_defaultValue,
|
|
@@ -29,7 +29,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
marko_template.meta = {
|
|
32
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
32
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.8/components/form-logout.marko",
|
|
33
33
|
tags: [
|
|
34
34
|
"@mindful-web/marko-web/components/browser-component.marko"
|
|
35
35
|
]
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.8/components/form-profile.marko",
|
|
6
6
|
marko_component = require("./form-profile.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
module_objectPath_module = require("@mindful-web/object-path"),
|
|
@@ -84,7 +84,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
84
84
|
}, marko_component);
|
|
85
85
|
|
|
86
86
|
marko_template.meta = {
|
|
87
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
87
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.8/components/form-profile.marko",
|
|
88
88
|
component: "./form-profile.marko",
|
|
89
89
|
tags: [
|
|
90
90
|
"@mindful-web/marko-web/components/browser-component.marko",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.8/components/form-register.marko",
|
|
6
6
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
7
7
|
marko_assign = require("marko/dist/runtime/helpers/assign"),
|
|
8
8
|
marko_web_identity_x_form_login_template = require("./form-login.marko"),
|
|
@@ -25,7 +25,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
marko_template.meta = {
|
|
28
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
28
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.8/components/form-register.marko",
|
|
29
29
|
tags: [
|
|
30
30
|
"./form-login.marko"
|
|
31
31
|
]
|
|
@@ -2,6 +2,7 @@ $ const { req } = out.global;
|
|
|
2
2
|
$ const config = req.identityX && req.identityX.config;
|
|
3
3
|
$ const { clientId, autoPrompt } = config ? config.getAsObject('googleAuth') : {};
|
|
4
4
|
$ const callbackName = 'handleGoogleOneTapCredential';
|
|
5
|
+
$ const loginUrl = config ? config.getEndpointFor('login') : '/user/login';
|
|
5
6
|
$ const initScript = `
|
|
6
7
|
(function() {
|
|
7
8
|
window.${callbackName} = function(response) {
|
|
@@ -12,24 +13,97 @@ $ const initScript = `
|
|
|
12
13
|
// GIS 'select_by' distinguishes the surface: a 'btn*' value means the
|
|
13
14
|
// rendered "Sign in with Google" button, 'user*'/'auto' means the One-Tap
|
|
14
15
|
// prompt. Used to label the p1events authentication event accordingly.
|
|
15
|
-
var
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
var isButton = String(response.select_by || '').indexOf('btn') === 0;
|
|
17
|
+
var eventLabel = isButton ? 'Google Sign In' : 'Google One Tap';
|
|
18
|
+
|
|
19
|
+
// Every failure path routes through here. The site-wide floating One-Tap
|
|
20
|
+
// prompt renders outside any form, so there is no '#google-sign-in-error'
|
|
21
|
+
// element on the page — previously that meant a failed sign-in did nothing
|
|
22
|
+
// at all: GIS closed the prompt, the user assumed success, and only found
|
|
23
|
+
// out they were still anonymous on the next page load. Always log, and
|
|
24
|
+
// fall back to the login page when there's nowhere to show the message.
|
|
25
|
+
var authenticated = false;
|
|
26
|
+
function fail(message, detail) {
|
|
27
|
+
if (window.console && window.console.error) {
|
|
28
|
+
window.console.error('[identity-x google-auth] ' + message, detail);
|
|
29
|
+
}
|
|
30
|
+
// There is no New Relic Browser agent on these sites, so a client-side
|
|
31
|
+
// failure reaches no backend at all unless we send one. p1events is
|
|
32
|
+
// already on every page -- mirror the success event so failed sign-ins
|
|
33
|
+
// are countable next to successful ones rather than invisible.
|
|
34
|
+
if (window.p1events) {
|
|
35
|
+
window.p1events('track', {
|
|
36
|
+
category: 'Identity',
|
|
37
|
+
action: 'Authenticate Failed',
|
|
38
|
+
label: eventLabel,
|
|
39
|
+
props: {
|
|
40
|
+
actionSource: 'google-one-tap',
|
|
41
|
+
surface: isButton ? 'button' : 'prompt',
|
|
42
|
+
message: message,
|
|
43
|
+
status: detail && detail.status ? detail.status : null,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// The session cookie is already set — a throw in the post-success
|
|
48
|
+
// analytics/redirect code must not bounce an authenticated user to login.
|
|
49
|
+
if (authenticated) return;
|
|
50
|
+
if (errorEl) {
|
|
51
|
+
errorEl.textContent = message;
|
|
52
|
+
errorEl.style.display = 'block';
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
var returnTo = window.location.pathname + window.location.search;
|
|
56
|
+
window.location.href = ${JSON.stringify(loginUrl)} + '?redirectTo=' + encodeURIComponent(returnTo);
|
|
57
|
+
}
|
|
18
58
|
|
|
19
59
|
fetch('/__idx/google', {
|
|
20
60
|
method: 'POST',
|
|
21
61
|
headers: { 'Content-Type': 'application/json' },
|
|
22
|
-
body: JSON.stringify({
|
|
62
|
+
body: JSON.stringify({
|
|
63
|
+
credential: response.credential,
|
|
64
|
+
redirectTo: redirectTo,
|
|
65
|
+
// Lets the server tag its telemetry with the originating surface --
|
|
66
|
+
// both post here and are otherwise indistinguishable server-side.
|
|
67
|
+
surface: isButton ? 'button' : 'prompt',
|
|
68
|
+
}),
|
|
23
69
|
})
|
|
24
|
-
|
|
25
|
-
|
|
70
|
+
// Parse defensively: a 5xx from the idx router returns
|
|
71
|
+
// { error, status, message } (no 'ok'), and an upstream proxy can return
|
|
72
|
+
// HTML that would otherwise blow up res.json() into the generic catch.
|
|
73
|
+
.then(function(res) {
|
|
74
|
+
return res.text().then(function(text) {
|
|
75
|
+
var parsed = {};
|
|
76
|
+
try { parsed = JSON.parse(text); } catch (e) { /* non-JSON response */ }
|
|
77
|
+
return { status: res.status, redirected: res.redirected, url: res.url, data: parsed };
|
|
78
|
+
});
|
|
79
|
+
})
|
|
80
|
+
.then(function(result) {
|
|
81
|
+
var data = result.data;
|
|
26
82
|
if (!data.ok) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
83
|
+
// A 301/302 on this POST (host canonicalization — apex -> www, or
|
|
84
|
+
// http -> https) is re-issued by the browser as a GET with the body
|
|
85
|
+
// dropped. The idx router only registers POST /__idx/google, so the
|
|
86
|
+
// redirected request falls through to a 404 and the sign-in is lost.
|
|
87
|
+
// Call that out by name rather than reporting a generic failure.
|
|
88
|
+
if (result.redirected) {
|
|
89
|
+
fail(
|
|
90
|
+
'Google sign-in failed: the request was redirected to ' + result.url
|
|
91
|
+
+ ', which turns it into a GET and drops the credential. '
|
|
92
|
+
+ 'Serve this page from the canonical host.',
|
|
93
|
+
{ status: result.status }
|
|
94
|
+
);
|
|
95
|
+
return;
|
|
30
96
|
}
|
|
97
|
+
fail(
|
|
98
|
+
data.error || data.message || 'Google sign-in failed. Please try again.',
|
|
99
|
+
{ status: result.status }
|
|
100
|
+
);
|
|
31
101
|
return;
|
|
32
102
|
}
|
|
103
|
+
authenticated = true;
|
|
104
|
+
var eventData = data.additionalEventData || {};
|
|
105
|
+
var autoSignups = eventData.autoSignups || [];
|
|
106
|
+
|
|
33
107
|
if (window.p1events) {
|
|
34
108
|
// De-anonymize the session to the app user, then record the
|
|
35
109
|
// authentication — mirrors the identity-x conversion events.
|
|
@@ -40,36 +114,92 @@ $ const initScript = `
|
|
|
40
114
|
label: eventLabel,
|
|
41
115
|
props: { idxEntity: data.entity, actionSource: 'google-one-tap' },
|
|
42
116
|
});
|
|
117
|
+
// One 'Subscription / Subscribe' event per newsletter the server
|
|
118
|
+
// opted this user into, matching what browser/index.js emits for the
|
|
119
|
+
// 'identity-x-auto-signup' bus event in the email-link flow.
|
|
120
|
+
for (var pi = 0; pi < autoSignups.length; pi += 1) {
|
|
121
|
+
window.p1events('track', {
|
|
122
|
+
category: 'Subscription',
|
|
123
|
+
action: 'Subscribe',
|
|
124
|
+
props: {
|
|
125
|
+
idxEntity: data.entity,
|
|
126
|
+
actionSource: autoSignups[pi].actionSource || data.loginSource,
|
|
127
|
+
subscriptionEntity: autoSignups[pi].subscriptionEntity,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
43
131
|
}
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
132
|
+
|
|
133
|
+
// Push to dataLayer for GTM/GA4. Assign rather than guard: GTM can be
|
|
134
|
+
// deferred behind the script loader, and a One-Tap sign-in that lands
|
|
135
|
+
// before the container initializes would otherwise drop its events
|
|
136
|
+
// silently. Pushing into a plain array is safe — GTM replays whatever
|
|
137
|
+
// is already there when it loads.
|
|
138
|
+
window.dataLayer = window.dataLayer || [];
|
|
139
|
+
|
|
140
|
+
// Mirror the payload the standard email-link authenticate flow builds
|
|
141
|
+
// (browser/authenticate.vue + mixins/global-event-emitter.js) so GTM
|
|
142
|
+
// triggers and dataLayer variables resolve identically. That flow sets
|
|
143
|
+
// actionSource, loginSource AND source to the same value; a variable
|
|
144
|
+
// reading any one of the three has to work here too.
|
|
145
|
+
var idxAuthPayload = {
|
|
146
|
+
event: 'identity-x-authenticated',
|
|
147
|
+
'identity-x': {
|
|
51
148
|
event: 'identity-x-authenticated',
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
149
|
+
ok: true,
|
|
150
|
+
applicationId: data.applicationId,
|
|
151
|
+
user: data.user,
|
|
152
|
+
id: data.user.id,
|
|
153
|
+
email: data.user.email,
|
|
154
|
+
verifiedCount: data.user.verifiedCount,
|
|
155
|
+
entity: data.entity,
|
|
156
|
+
requiresUserInput: data.requiresUserInput,
|
|
157
|
+
createdNewUser: eventData.createdNewUser,
|
|
158
|
+
additionalEventData: eventData,
|
|
159
|
+
actionSource: data.loginSource,
|
|
160
|
+
loginSource: data.loginSource,
|
|
161
|
+
source: data.loginSource,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
window.dataLayer.push(idxAuthPayload);
|
|
165
|
+
|
|
166
|
+
// The 'subscribe' conversion keys off 'identity-x-auto-signup', not
|
|
167
|
+
// 'identity-x-authenticated' — one event per newsletter product the
|
|
168
|
+
// Omeda hook subscribed the user to. Shape matches
|
|
169
|
+
// mixins/global-auto-signup-event-emitter.js -> browser/index.js:
|
|
170
|
+
// { entity, ...autoSignup, event }.
|
|
171
|
+
for (var i = 0; i < autoSignups.length; i += 1) {
|
|
172
|
+
var signupData = { entity: data.entity };
|
|
173
|
+
var autoSignup = autoSignups[i];
|
|
174
|
+
for (var key in autoSignup) {
|
|
175
|
+
if (Object.prototype.hasOwnProperty.call(autoSignup, key)) {
|
|
176
|
+
signupData[key] = autoSignup[key];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
signupData.event = 'identity-x-auto-signup';
|
|
180
|
+
window.dataLayer.push({
|
|
181
|
+
event: 'identity-x-auto-signup',
|
|
182
|
+
'identity-x': signupData,
|
|
59
183
|
});
|
|
60
184
|
}
|
|
185
|
+
|
|
186
|
+
var href;
|
|
61
187
|
if (data.requiresUserInput) {
|
|
62
188
|
var destination = data.redirectTo || window.location.pathname;
|
|
63
|
-
|
|
189
|
+
href = '/user/profile?returnTo=' + encodeURIComponent(destination);
|
|
64
190
|
} else {
|
|
65
|
-
|
|
191
|
+
href = data.redirectTo || window.location.href;
|
|
66
192
|
}
|
|
193
|
+
// Give GTM a moment to dispatch the tags above before tearing down the
|
|
194
|
+
// document. The email-link flow navigates in the same tick, but it does
|
|
195
|
+
// so from /user/authenticate after a full page load, where the container
|
|
196
|
+
// is long since warm; a One-Tap sign-in can resolve much earlier in a
|
|
197
|
+
// page's life. A short delay is imperceptible next to the sign-in
|
|
198
|
+
// round-trip and cheaper than losing the conversion.
|
|
199
|
+
window.setTimeout(function() { window.location.href = href; }, 300);
|
|
67
200
|
})
|
|
68
|
-
.catch(function() {
|
|
69
|
-
|
|
70
|
-
errorEl.textContent = 'An error occurred during Google sign-in. Please try again.';
|
|
71
|
-
errorEl.style.display = 'block';
|
|
72
|
-
}
|
|
201
|
+
.catch(function(e) {
|
|
202
|
+
fail('An error occurred during Google sign-in. Please try again.', e);
|
|
73
203
|
});
|
|
74
204
|
};
|
|
75
205
|
})();
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.8/components/google-init.marko",
|
|
6
6
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
7
7
|
marko_attr = require("marko/dist/runtime/html/helpers/attr"),
|
|
8
8
|
marko_str = require("marko/dist/runtime/helpers/to-string");
|
|
@@ -18,6 +18,8 @@ function render(input, out, __component, component, state) {
|
|
|
18
18
|
|
|
19
19
|
const callbackName = 'handleGoogleOneTapCredential';
|
|
20
20
|
|
|
21
|
+
const loginUrl = config ? config.getEndpointFor('login') : '/user/login';
|
|
22
|
+
|
|
21
23
|
const initScript = `
|
|
22
24
|
(function() {
|
|
23
25
|
window.${callbackName} = function(response) {
|
|
@@ -28,24 +30,97 @@ function render(input, out, __component, component, state) {
|
|
|
28
30
|
// GIS 'select_by' distinguishes the surface: a 'btn*' value means the
|
|
29
31
|
// rendered "Sign in with Google" button, 'user*'/'auto' means the One-Tap
|
|
30
32
|
// prompt. Used to label the p1events authentication event accordingly.
|
|
31
|
-
var
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
var isButton = String(response.select_by || '').indexOf('btn') === 0;
|
|
34
|
+
var eventLabel = isButton ? 'Google Sign In' : 'Google One Tap';
|
|
35
|
+
|
|
36
|
+
// Every failure path routes through here. The site-wide floating One-Tap
|
|
37
|
+
// prompt renders outside any form, so there is no '#google-sign-in-error'
|
|
38
|
+
// element on the page — previously that meant a failed sign-in did nothing
|
|
39
|
+
// at all: GIS closed the prompt, the user assumed success, and only found
|
|
40
|
+
// out they were still anonymous on the next page load. Always log, and
|
|
41
|
+
// fall back to the login page when there's nowhere to show the message.
|
|
42
|
+
var authenticated = false;
|
|
43
|
+
function fail(message, detail) {
|
|
44
|
+
if (window.console && window.console.error) {
|
|
45
|
+
window.console.error('[identity-x google-auth] ' + message, detail);
|
|
46
|
+
}
|
|
47
|
+
// There is no New Relic Browser agent on these sites, so a client-side
|
|
48
|
+
// failure reaches no backend at all unless we send one. p1events is
|
|
49
|
+
// already on every page -- mirror the success event so failed sign-ins
|
|
50
|
+
// are countable next to successful ones rather than invisible.
|
|
51
|
+
if (window.p1events) {
|
|
52
|
+
window.p1events('track', {
|
|
53
|
+
category: 'Identity',
|
|
54
|
+
action: 'Authenticate Failed',
|
|
55
|
+
label: eventLabel,
|
|
56
|
+
props: {
|
|
57
|
+
actionSource: 'google-one-tap',
|
|
58
|
+
surface: isButton ? 'button' : 'prompt',
|
|
59
|
+
message: message,
|
|
60
|
+
status: detail && detail.status ? detail.status : null,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// The session cookie is already set — a throw in the post-success
|
|
65
|
+
// analytics/redirect code must not bounce an authenticated user to login.
|
|
66
|
+
if (authenticated) return;
|
|
67
|
+
if (errorEl) {
|
|
68
|
+
errorEl.textContent = message;
|
|
69
|
+
errorEl.style.display = 'block';
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
var returnTo = window.location.pathname + window.location.search;
|
|
73
|
+
window.location.href = ${JSON.stringify(loginUrl)} + '?redirectTo=' + encodeURIComponent(returnTo);
|
|
74
|
+
}
|
|
34
75
|
|
|
35
76
|
fetch('/__idx/google', {
|
|
36
77
|
method: 'POST',
|
|
37
78
|
headers: { 'Content-Type': 'application/json' },
|
|
38
|
-
body: JSON.stringify({
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
credential: response.credential,
|
|
81
|
+
redirectTo: redirectTo,
|
|
82
|
+
// Lets the server tag its telemetry with the originating surface --
|
|
83
|
+
// both post here and are otherwise indistinguishable server-side.
|
|
84
|
+
surface: isButton ? 'button' : 'prompt',
|
|
85
|
+
}),
|
|
39
86
|
})
|
|
40
|
-
|
|
41
|
-
|
|
87
|
+
// Parse defensively: a 5xx from the idx router returns
|
|
88
|
+
// { error, status, message } (no 'ok'), and an upstream proxy can return
|
|
89
|
+
// HTML that would otherwise blow up res.json() into the generic catch.
|
|
90
|
+
.then(function(res) {
|
|
91
|
+
return res.text().then(function(text) {
|
|
92
|
+
var parsed = {};
|
|
93
|
+
try { parsed = JSON.parse(text); } catch (e) { /* non-JSON response */ }
|
|
94
|
+
return { status: res.status, redirected: res.redirected, url: res.url, data: parsed };
|
|
95
|
+
});
|
|
96
|
+
})
|
|
97
|
+
.then(function(result) {
|
|
98
|
+
var data = result.data;
|
|
42
99
|
if (!data.ok) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
100
|
+
// A 301/302 on this POST (host canonicalization — apex -> www, or
|
|
101
|
+
// http -> https) is re-issued by the browser as a GET with the body
|
|
102
|
+
// dropped. The idx router only registers POST /__idx/google, so the
|
|
103
|
+
// redirected request falls through to a 404 and the sign-in is lost.
|
|
104
|
+
// Call that out by name rather than reporting a generic failure.
|
|
105
|
+
if (result.redirected) {
|
|
106
|
+
fail(
|
|
107
|
+
'Google sign-in failed: the request was redirected to ' + result.url
|
|
108
|
+
+ ', which turns it into a GET and drops the credential. '
|
|
109
|
+
+ 'Serve this page from the canonical host.',
|
|
110
|
+
{ status: result.status }
|
|
111
|
+
);
|
|
112
|
+
return;
|
|
46
113
|
}
|
|
114
|
+
fail(
|
|
115
|
+
data.error || data.message || 'Google sign-in failed. Please try again.',
|
|
116
|
+
{ status: result.status }
|
|
117
|
+
);
|
|
47
118
|
return;
|
|
48
119
|
}
|
|
120
|
+
authenticated = true;
|
|
121
|
+
var eventData = data.additionalEventData || {};
|
|
122
|
+
var autoSignups = eventData.autoSignups || [];
|
|
123
|
+
|
|
49
124
|
if (window.p1events) {
|
|
50
125
|
// De-anonymize the session to the app user, then record the
|
|
51
126
|
// authentication — mirrors the identity-x conversion events.
|
|
@@ -56,36 +131,92 @@ function render(input, out, __component, component, state) {
|
|
|
56
131
|
label: eventLabel,
|
|
57
132
|
props: { idxEntity: data.entity, actionSource: 'google-one-tap' },
|
|
58
133
|
});
|
|
134
|
+
// One 'Subscription / Subscribe' event per newsletter the server
|
|
135
|
+
// opted this user into, matching what browser/index.js emits for the
|
|
136
|
+
// 'identity-x-auto-signup' bus event in the email-link flow.
|
|
137
|
+
for (var pi = 0; pi < autoSignups.length; pi += 1) {
|
|
138
|
+
window.p1events('track', {
|
|
139
|
+
category: 'Subscription',
|
|
140
|
+
action: 'Subscribe',
|
|
141
|
+
props: {
|
|
142
|
+
idxEntity: data.entity,
|
|
143
|
+
actionSource: autoSignups[pi].actionSource || data.loginSource,
|
|
144
|
+
subscriptionEntity: autoSignups[pi].subscriptionEntity,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
59
148
|
}
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
149
|
+
|
|
150
|
+
// Push to dataLayer for GTM/GA4. Assign rather than guard: GTM can be
|
|
151
|
+
// deferred behind the script loader, and a One-Tap sign-in that lands
|
|
152
|
+
// before the container initializes would otherwise drop its events
|
|
153
|
+
// silently. Pushing into a plain array is safe — GTM replays whatever
|
|
154
|
+
// is already there when it loads.
|
|
155
|
+
window.dataLayer = window.dataLayer || [];
|
|
156
|
+
|
|
157
|
+
// Mirror the payload the standard email-link authenticate flow builds
|
|
158
|
+
// (browser/authenticate.vue + mixins/global-event-emitter.js) so GTM
|
|
159
|
+
// triggers and dataLayer variables resolve identically. That flow sets
|
|
160
|
+
// actionSource, loginSource AND source to the same value; a variable
|
|
161
|
+
// reading any one of the three has to work here too.
|
|
162
|
+
var idxAuthPayload = {
|
|
163
|
+
event: 'identity-x-authenticated',
|
|
164
|
+
'identity-x': {
|
|
67
165
|
event: 'identity-x-authenticated',
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
166
|
+
ok: true,
|
|
167
|
+
applicationId: data.applicationId,
|
|
168
|
+
user: data.user,
|
|
169
|
+
id: data.user.id,
|
|
170
|
+
email: data.user.email,
|
|
171
|
+
verifiedCount: data.user.verifiedCount,
|
|
172
|
+
entity: data.entity,
|
|
173
|
+
requiresUserInput: data.requiresUserInput,
|
|
174
|
+
createdNewUser: eventData.createdNewUser,
|
|
175
|
+
additionalEventData: eventData,
|
|
176
|
+
actionSource: data.loginSource,
|
|
177
|
+
loginSource: data.loginSource,
|
|
178
|
+
source: data.loginSource,
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
window.dataLayer.push(idxAuthPayload);
|
|
182
|
+
|
|
183
|
+
// The 'subscribe' conversion keys off 'identity-x-auto-signup', not
|
|
184
|
+
// 'identity-x-authenticated' — one event per newsletter product the
|
|
185
|
+
// Omeda hook subscribed the user to. Shape matches
|
|
186
|
+
// mixins/global-auto-signup-event-emitter.js -> browser/index.js:
|
|
187
|
+
// { entity, ...autoSignup, event }.
|
|
188
|
+
for (var i = 0; i < autoSignups.length; i += 1) {
|
|
189
|
+
var signupData = { entity: data.entity };
|
|
190
|
+
var autoSignup = autoSignups[i];
|
|
191
|
+
for (var key in autoSignup) {
|
|
192
|
+
if (Object.prototype.hasOwnProperty.call(autoSignup, key)) {
|
|
193
|
+
signupData[key] = autoSignup[key];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
signupData.event = 'identity-x-auto-signup';
|
|
197
|
+
window.dataLayer.push({
|
|
198
|
+
event: 'identity-x-auto-signup',
|
|
199
|
+
'identity-x': signupData,
|
|
75
200
|
});
|
|
76
201
|
}
|
|
202
|
+
|
|
203
|
+
var href;
|
|
77
204
|
if (data.requiresUserInput) {
|
|
78
205
|
var destination = data.redirectTo || window.location.pathname;
|
|
79
|
-
|
|
206
|
+
href = '/user/profile?returnTo=' + encodeURIComponent(destination);
|
|
80
207
|
} else {
|
|
81
|
-
|
|
208
|
+
href = data.redirectTo || window.location.href;
|
|
82
209
|
}
|
|
210
|
+
// Give GTM a moment to dispatch the tags above before tearing down the
|
|
211
|
+
// document. The email-link flow navigates in the same tick, but it does
|
|
212
|
+
// so from /user/authenticate after a full page load, where the container
|
|
213
|
+
// is long since warm; a One-Tap sign-in can resolve much earlier in a
|
|
214
|
+
// page's life. A short delay is imperceptible next to the sign-in
|
|
215
|
+
// round-trip and cheaper than losing the conversion.
|
|
216
|
+
window.setTimeout(function() { window.location.href = href; }, 300);
|
|
83
217
|
})
|
|
84
|
-
.catch(function() {
|
|
85
|
-
|
|
86
|
-
errorEl.textContent = 'An error occurred during Google sign-in. Please try again.';
|
|
87
|
-
errorEl.style.display = 'block';
|
|
88
|
-
}
|
|
218
|
+
.catch(function(e) {
|
|
219
|
+
fail('An error occurred during Google sign-in. Please try again.', e);
|
|
89
220
|
});
|
|
90
221
|
};
|
|
91
222
|
})();
|
|
@@ -108,5 +239,5 @@ marko_template._ = marko_renderer(render, {
|
|
|
108
239
|
});
|
|
109
240
|
|
|
110
241
|
marko_template.meta = {
|
|
111
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
242
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.8/components/google-init.marko"
|
|
112
243
|
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.8/components/google-sign-in-button.marko",
|
|
6
6
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
7
7
|
helpers_escape_xml = require("marko/dist/runtime/html/helpers/escape-xml"),
|
|
8
8
|
marko_escapeXml = helpers_escape_xml.x,
|
|
@@ -40,5 +40,5 @@ marko_template._ = marko_renderer(render, {
|
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
marko_template.meta = {
|
|
43
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
43
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.8/components/google-sign-in-button.marko"
|
|
44
44
|
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.8/components/identify.marko",
|
|
6
6
|
marko_component = require("./identify.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
module_objectPath_module = require("@mindful-web/object-path"),
|
|
@@ -50,7 +50,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
50
50
|
}, marko_component);
|
|
51
51
|
|
|
52
52
|
marko_template.meta = {
|
|
53
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
53
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.8/components/identify.marko",
|
|
54
54
|
component: "./identify.marko",
|
|
55
55
|
tags: [
|
|
56
56
|
"@mindful-web/marko-web-gtm/components/push.marko",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.8/components/non-auth-identify.marko",
|
|
6
6
|
marko_component = require("./non-auth-identify.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
marko_dynamicTag = require("marko/dist/runtime/helpers/dynamic-tag"),
|
|
@@ -45,7 +45,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
45
45
|
}, marko_component);
|
|
46
46
|
|
|
47
47
|
marko_template.meta = {
|
|
48
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
48
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.8/components/non-auth-identify.marko",
|
|
49
49
|
component: "./non-auth-identify.marko",
|
|
50
50
|
tags: [
|
|
51
51
|
"@mindful-web/marko-core/components/resolve.marko"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.8/components/subscribe.marko",
|
|
6
6
|
marko_component = require("./subscribe.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
module_objectPath_module = require("@mindful-web/object-path"),
|
|
@@ -73,7 +73,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
73
73
|
}, marko_component);
|
|
74
74
|
|
|
75
75
|
marko_template.meta = {
|
|
76
|
-
id: "/@mindful-web/marko-web-identity-x$1.79.
|
|
76
|
+
id: "/@mindful-web/marko-web-identity-x$1.79.8/components/subscribe.marko",
|
|
77
77
|
component: "./subscribe.marko",
|
|
78
78
|
tags: [
|
|
79
79
|
"@mindful-web/marko-web/components/browser-component.marko",
|
package/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { get, getAsArray, getAsObject } = require('@mindful-web/object-path');
|
|
2
2
|
const { isFunction: isFn } = require('@mindful-web/utils');
|
|
3
3
|
const validHooks = require('./hooks');
|
|
4
|
+
const defaultAddRequestAttributes = require('./utils/add-custom-attributes');
|
|
4
5
|
|
|
5
6
|
const { log } = console;
|
|
6
7
|
|
|
@@ -15,7 +16,17 @@ class IdentityXConfiguration {
|
|
|
15
16
|
* @param {string[]} [options.requiredCreateFieldRows] Required fields, when creating a new user.
|
|
16
17
|
* @param {string[]} [options.activeCustomFieldIds] Limit displayed custom fields, if present.
|
|
17
18
|
* @param {string[]} [options.hiddenFields] The fields to hide from the profile.
|
|
18
|
-
* @param {function} [options.onHookError]
|
|
19
|
+
* @param {function} [options.onHookError] Reporter for non-fatal errors. Called as
|
|
20
|
+
* `(error, customAttributes)` — matches `newrelic.noticeError`.
|
|
21
|
+
* @param {function} [options.addRequestAttributes] Sink for per-request telemetry,
|
|
22
|
+
* called with a flat object of primitives. **Defaults to tagging the current New
|
|
23
|
+
* Relic transaction** when the app has already loaded the agent (every marko-web
|
|
24
|
+
* site does, at boot) — so the attributes ride on both the `Transaction` and any
|
|
25
|
+
* `TransactionError` it produces, and a 500 arrives already saying which step
|
|
26
|
+
* failed and for whom. No site configuration required; inert without New Relic.
|
|
27
|
+
* Deliberately not `noticeError` — that would record a second error alongside the
|
|
28
|
+
* one the error handler already captures, and double the counts. Pass your own
|
|
29
|
+
* function to override, or a no-op to disable.
|
|
19
30
|
* @param {object} [options.googleAuth] Google Sign-In config. When `clientId` is
|
|
20
31
|
* set, the One-Tap prompt / "Sign in with Google" button render in the auth
|
|
21
32
|
* templates and `POST /__idx/google` accepts credentials. Dormant otherwise.
|
|
@@ -40,6 +51,7 @@ class IdentityXConfiguration {
|
|
|
40
51
|
gtmUserFields = {},
|
|
41
52
|
googleAuth = {},
|
|
42
53
|
onHookError,
|
|
54
|
+
addRequestAttributes,
|
|
43
55
|
...rest
|
|
44
56
|
} = {}) {
|
|
45
57
|
if (!appId) throw new Error('Unable to configure IdentityX: no Application ID was provided.');
|
|
@@ -58,13 +70,18 @@ class IdentityXConfiguration {
|
|
|
58
70
|
booleanQuestionsLabel,
|
|
59
71
|
gtmUserFields,
|
|
60
72
|
googleAuth,
|
|
61
|
-
|
|
73
|
+
// Extra args are forwarded so callers can pass structured context —
|
|
74
|
+
// `newrelic.noticeError(error, customAttributes)` takes it natively.
|
|
75
|
+
onHookError: (e, ...args) => {
|
|
62
76
|
if (process.env.NODE_ENV === 'development') {
|
|
63
|
-
log('ERROR IN IDENTITY-X HOOK', e);
|
|
77
|
+
log('ERROR IN IDENTITY-X HOOK', e, ...args);
|
|
64
78
|
if (e.networkError) log('Network Error', get(e, 'networkError.result.errors.0'));
|
|
65
79
|
}
|
|
66
|
-
if (isFn(onHookError)) onHookError(e);
|
|
80
|
+
if (isFn(onHookError)) onHookError(e, ...args);
|
|
67
81
|
},
|
|
82
|
+
addRequestAttributes: isFn(addRequestAttributes)
|
|
83
|
+
? addRequestAttributes
|
|
84
|
+
: defaultAddRequestAttributes,
|
|
68
85
|
...rest,
|
|
69
86
|
};
|
|
70
87
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindful-web/marko-web-identity-x",
|
|
3
|
-
"version": "1.79.
|
|
3
|
+
"version": "1.79.8",
|
|
4
4
|
"description": "Marko Wrapper for IdentityX",
|
|
5
5
|
"repository": "https://github.com/parameter1/mindful-web/tree/main/packages/marko-web-identity-x",
|
|
6
6
|
"author": "Josh Worden <josh@parameter1.com>",
|
|
@@ -38,5 +38,5 @@
|
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "aafeaa99860062c3faa70b67bab0e69b49d13632"
|
|
42
42
|
}
|
package/routes/google.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const gql = require('graphql-tag');
|
|
2
2
|
const { OAuth2Client } = require('google-auth-library');
|
|
3
|
-
const { asyncRoute } = require('@mindful-web/utils');
|
|
3
|
+
const { asArray, asyncRoute } = require('@mindful-web/utils');
|
|
4
4
|
const IMPERSONATE_APP_USER = require('../api/mutations/impersonate-app-user');
|
|
5
5
|
const tokenCookie = require('../utils/token-cookie');
|
|
6
6
|
const contextCookie = require('../utils/context-cookie');
|
|
@@ -14,6 +14,16 @@ const UPDATE_OWN_APP_USER = gql`
|
|
|
14
14
|
|
|
15
15
|
const isEmpty = (v) => v == null || v === '';
|
|
16
16
|
|
|
17
|
+
// Cache one OAuth2Client per client id. `verifyIdToken` fetches (and caches on
|
|
18
|
+
// the *instance*) Google's public signing certs, so constructing a client per
|
|
19
|
+
// request re-fetched them on every sign-in — turning a transient network blip
|
|
20
|
+
// or rate-limit at Google into a failed sign-in.
|
|
21
|
+
const oauthClients = new Map();
|
|
22
|
+
const getOAuthClient = (clientId) => {
|
|
23
|
+
if (!oauthClients.has(clientId)) oauthClients.set(clientId, new OAuth2Client(clientId));
|
|
24
|
+
return oauthClients.get(clientId);
|
|
25
|
+
};
|
|
26
|
+
|
|
17
27
|
const LOGIN_SOURCE = 'google-one-tap';
|
|
18
28
|
|
|
19
29
|
// Namespace under which the Google account id (the JWT `sub`) is stored as an
|
|
@@ -50,7 +60,21 @@ module.exports = asyncRoute(async (req, res) => {
|
|
|
50
60
|
const { config } = identityX;
|
|
51
61
|
const onError = config.get('onHookError') || defaultOnError;
|
|
52
62
|
|
|
63
|
+
// Tag the transaction as the route progresses. Whatever step is recorded when
|
|
64
|
+
// the request dies is the step that failed, so an error arrives already
|
|
65
|
+
// localized instead of being a bare GraphQL message to reverse-engineer.
|
|
66
|
+
const addAttrs = config.get('addRequestAttributes') || (() => {});
|
|
67
|
+
const setStep = (step) => addAttrs({ idxGoogleStep: step });
|
|
68
|
+
|
|
53
69
|
const { clientId, missingFieldsBehavior = 'profile-gate' } = config.getAsObject('googleAuth');
|
|
70
|
+
// `surface` is the GIS `select_by` bucket ('button' | 'prompt'), sent by the
|
|
71
|
+
// client. Both surfaces share a route, a loginSource and an impersonation
|
|
72
|
+
// method, so without it there is no way to tell from the server whether the
|
|
73
|
+
// rendered button works independently of the One-Tap prompt.
|
|
74
|
+
addAttrs({
|
|
75
|
+
idxGoogleSurface: body.surface === 'button' ? 'button' : 'prompt',
|
|
76
|
+
idxGoogleMissingFieldsBehavior: missingFieldsBehavior,
|
|
77
|
+
});
|
|
54
78
|
|
|
55
79
|
if (!clientId) {
|
|
56
80
|
res.status(400).json({ ok: false, error: 'Google Sign-In is not configured for this site.' });
|
|
@@ -62,7 +86,8 @@ module.exports = asyncRoute(async (req, res) => {
|
|
|
62
86
|
}
|
|
63
87
|
|
|
64
88
|
// Verify the Google JWT and extract user info.
|
|
65
|
-
|
|
89
|
+
setStep('verify-credential');
|
|
90
|
+
const oauthClient = getOAuthClient(clientId);
|
|
66
91
|
const ticket = await oauthClient.verifyIdToken({ idToken: credential, audience: clientId });
|
|
67
92
|
const googlePayload = ticket.getPayload();
|
|
68
93
|
const {
|
|
@@ -75,11 +100,14 @@ module.exports = asyncRoute(async (req, res) => {
|
|
|
75
100
|
if (!email) throw new Error('Google credential did not include an email address.');
|
|
76
101
|
|
|
77
102
|
// Find or create the identity-x user.
|
|
103
|
+
setStep('load-app-user');
|
|
78
104
|
let appUser = await identityX.loadAppUserByEmail(email);
|
|
79
105
|
const createdNewUser = !appUser;
|
|
80
106
|
if (!appUser) {
|
|
107
|
+
setStep('create-app-user');
|
|
81
108
|
appUser = await identityX.createAppUser({ email });
|
|
82
109
|
}
|
|
110
|
+
addAttrs({ idxUserId: appUser.id, idxCreatedNewUser: createdNewUser });
|
|
83
111
|
|
|
84
112
|
// Persist the Google account id (the JWT `sub`) as an idx external id so the
|
|
85
113
|
// user is linked to its Google identity. Email stays the primary key; this is
|
|
@@ -92,6 +120,7 @@ module.exports = asyncRoute(async (req, res) => {
|
|
|
92
120
|
&& extId.identifier.value === googleSub
|
|
93
121
|
));
|
|
94
122
|
if (!alreadyLinked) {
|
|
123
|
+
setStep('link-google-sub');
|
|
95
124
|
try {
|
|
96
125
|
// If this sub is already attached to a different user (e.g. the Google
|
|
97
126
|
// account's email changed and a new user was created under it), report
|
|
@@ -104,7 +133,7 @@ module.exports = asyncRoute(async (req, res) => {
|
|
|
104
133
|
if (existing && existing.id !== appUser.id) {
|
|
105
134
|
onError(new Error(
|
|
106
135
|
`Google sub already linked to idx user ${existing.id}, not ${appUser.id}`,
|
|
107
|
-
));
|
|
136
|
+
), { idxGoogleStep: 'link-google-sub', idxUserId: appUser.id, idxConflictUserId: existing.id });
|
|
108
137
|
} else {
|
|
109
138
|
await identityX.addExternalUserId({
|
|
110
139
|
userId: appUser.id,
|
|
@@ -114,11 +143,14 @@ module.exports = asyncRoute(async (req, res) => {
|
|
|
114
143
|
}
|
|
115
144
|
} catch (e) {
|
|
116
145
|
// Supplementary linkage — never block authentication on a failure here.
|
|
117
|
-
|
|
146
|
+
// Reported with context so these stay distinguishable from fatal errors
|
|
147
|
+
// on the same route (they arrive on a 200).
|
|
148
|
+
onError(e, { idxGoogleStep: 'link-google-sub', idxUserId: appUser.id });
|
|
118
149
|
}
|
|
119
150
|
}
|
|
120
151
|
}
|
|
121
152
|
|
|
153
|
+
setStep('impersonate-app-user');
|
|
122
154
|
// Impersonate the user with verify: true — marks them as verified and returns
|
|
123
155
|
// a session token, bypassing the normal email-link flow.
|
|
124
156
|
const { data: impersonateData } = await identityX.client.mutate({
|
|
@@ -140,9 +172,13 @@ module.exports = asyncRoute(async (req, res) => {
|
|
|
140
172
|
|
|
141
173
|
// Re-init the Apollo client with the user's own session token so that
|
|
142
174
|
// updateOwnAppUser is authenticated as this user (not as the org admin).
|
|
143
|
-
|
|
175
|
+
// `setToken` takes the token *string* — passing the `{ id, value }` object
|
|
176
|
+
// produced an `AppUser [object Object]` authorization header, which failed
|
|
177
|
+
// every subsequent user-authenticated call (see updateOwnAppUser below).
|
|
178
|
+
identityX.setToken(authToken.value);
|
|
144
179
|
|
|
145
180
|
// Set name fields from Google as verified data if not already present.
|
|
181
|
+
setStep('update-own-app-user');
|
|
146
182
|
const needsNameUpdate = !appUser.givenName || !appUser.familyName;
|
|
147
183
|
if (needsNameUpdate && (givenName || familyName)) {
|
|
148
184
|
const nameInput = {};
|
|
@@ -155,6 +191,7 @@ module.exports = asyncRoute(async (req, res) => {
|
|
|
155
191
|
}
|
|
156
192
|
|
|
157
193
|
// Load the refreshed user so hooks and field checks have up-to-date data.
|
|
194
|
+
setStep('load-refreshed-user');
|
|
158
195
|
const user = await identityX.findUserById(appUser.id);
|
|
159
196
|
const entity = await identityX.generateEntityId({ userId: appUser.id });
|
|
160
197
|
|
|
@@ -169,31 +206,96 @@ module.exports = asyncRoute(async (req, res) => {
|
|
|
169
206
|
];
|
|
170
207
|
requiresUserInput = requiredFields.some((key) => isEmpty(user[key]));
|
|
171
208
|
}
|
|
209
|
+
addAttrs({ idxRequiresUserInput: requiresUserInput });
|
|
210
|
+
|
|
211
|
+
// Tenant Omeda formatters write `autoSignups` into their `additionalEventData`
|
|
212
|
+
// in place — one entry per newsletter product they opt the user into — and
|
|
213
|
+
// that object is what goes out in the response body. It is the only channel by
|
|
214
|
+
// which the browser learns a subscription was created; without it no
|
|
215
|
+
// `identity-x-auto-signup` event reaches GTM/GA4 (the "subscribe" conversion)
|
|
216
|
+
// or p1events, and One Tap looks like it subscribes nobody even though the
|
|
217
|
+
// Omeda opt-in ran fine.
|
|
218
|
+
//
|
|
219
|
+
// Give each hook its OWN object and merge after, rather than sharing one
|
|
220
|
+
// reference, because tenants disagree about where the write lives AND several
|
|
221
|
+
// write in both places:
|
|
222
|
+
// onLoginLinkSentFormatter only ....... ab-media, allured, encore360, pmmi
|
|
223
|
+
// onAuthenticationSuccessFormatter only fusable, cox-matthews
|
|
224
|
+
// BOTH ................................ industrial, ironmarkets, watt
|
|
225
|
+
// Every tenant *assigns* `autoSignups` rather than appending, so a shared
|
|
226
|
+
// reference would let the second hook clobber the first — silently dropping a
|
|
227
|
+
// whole set of subscribe events for the three both-hook repos. In the
|
|
228
|
+
// email-link flow the two hooks fire on separate requests (login vs
|
|
229
|
+
// authenticate) and each reports its own set, so the union is what preserves
|
|
230
|
+
// existing behaviour here.
|
|
231
|
+
const linkSentEventData = { createdNewUser };
|
|
172
232
|
|
|
173
233
|
// Fire onLoginLinkSent before onAuthenticationSuccess so that hooks wired to
|
|
174
234
|
// it (e.g. Omeda rapid-identify and subscription sync) run for Google users
|
|
175
235
|
// the same way they do for email-link users. This also populates
|
|
176
236
|
// user.externalIds with the Omeda encryptedCustomerId so the subsequent
|
|
177
237
|
// onAuthenticationSuccess hook can set oly_enc_id in the response.
|
|
238
|
+
setStep('hooks-login-link-sent');
|
|
178
239
|
await callHooksFor(identityX, 'onLoginLinkSent', {
|
|
179
240
|
req,
|
|
180
241
|
source: LOGIN_SOURCE,
|
|
181
242
|
user,
|
|
243
|
+
additionalEventData: linkSentEventData,
|
|
182
244
|
});
|
|
183
245
|
|
|
184
246
|
// Re-fetch so onAuthenticationSuccess sees the externalIds written by
|
|
185
247
|
// the onLoginLinkSent hook (Omeda rapid-identify).
|
|
186
248
|
const userAfterLinkSent = await identityX.findUserById(appUser.id);
|
|
187
249
|
|
|
250
|
+
// `requiresUserInput` is exposed to this hook only. The onLoginLinkSent
|
|
251
|
+
// formatters read a truthy value as "skip the Omeda subscription lookup" and
|
|
252
|
+
// then opt the user into every configured deployment type without deduping
|
|
253
|
+
// against what they already hold — a phantom auto-signup, and an inflated GA4
|
|
254
|
+
// subscribe count, for an existing Omeda customer who merely lacks an idx
|
|
255
|
+
// required field. The email-link flow never exposes it there either:
|
|
256
|
+
// `routes/login.js` 400s with `requiresUserInput` before any hook runs.
|
|
257
|
+
const authEventData = { createdNewUser, requiresUserInput };
|
|
258
|
+
|
|
259
|
+
setStep('hooks-authentication-success');
|
|
188
260
|
await callHooksFor(identityX, 'onAuthenticationSuccess', {
|
|
189
261
|
req,
|
|
190
262
|
res,
|
|
191
263
|
user: userAfterLinkSent,
|
|
192
264
|
authToken,
|
|
193
265
|
loginSource: LOGIN_SOURCE,
|
|
194
|
-
additionalEventData:
|
|
266
|
+
additionalEventData: authEventData,
|
|
195
267
|
});
|
|
196
268
|
|
|
269
|
+
// Union of both hooks' auto-signups, deduped by product so a tenant that
|
|
270
|
+
// opts into the same newsletter from both hooks yields one subscribe event,
|
|
271
|
+
// not two.
|
|
272
|
+
const seenProducts = new Set();
|
|
273
|
+
const autoSignups = [
|
|
274
|
+
...asArray(linkSentEventData.autoSignups),
|
|
275
|
+
...asArray(authEventData.autoSignups),
|
|
276
|
+
]
|
|
277
|
+
.filter((signup) => {
|
|
278
|
+
const key = signup.productId != null
|
|
279
|
+
? `product:${signup.productId}`
|
|
280
|
+
: `entity:${signup.subscriptionEntity}`;
|
|
281
|
+
if (seenProducts.has(key)) return false;
|
|
282
|
+
seenProducts.add(key);
|
|
283
|
+
return true;
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const additionalEventData = {
|
|
287
|
+
...linkSentEventData,
|
|
288
|
+
...authEventData,
|
|
289
|
+
...(autoSignups.length && { autoSignups }),
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
// A new user with zero auto-signups is the shape this whole bug had: the
|
|
293
|
+
// sign-in succeeds, Omeda is updated, and GA4 records no conversion. Surface
|
|
294
|
+
// the count so a regression is a visible number on the transaction rather
|
|
295
|
+
// than something a stakeholder notices in a report weeks later.
|
|
296
|
+
addAttrs({ idxAutoSignupCount: autoSignups.length });
|
|
297
|
+
|
|
298
|
+
setStep('complete');
|
|
197
299
|
res.json({
|
|
198
300
|
ok: true,
|
|
199
301
|
requiresUserInput,
|
|
@@ -201,6 +303,7 @@ module.exports = asyncRoute(async (req, res) => {
|
|
|
201
303
|
applicationId: config.getAppId(),
|
|
202
304
|
user: userAfterLinkSent,
|
|
203
305
|
loginSource: LOGIN_SOURCE,
|
|
306
|
+
additionalEventData,
|
|
204
307
|
entity,
|
|
205
308
|
});
|
|
206
309
|
});
|
package/service.js
CHANGED
|
@@ -355,8 +355,9 @@ class IdentityX {
|
|
|
355
355
|
}
|
|
356
356
|
|
|
357
357
|
/**
|
|
358
|
+
* Sets the active app-user token and re-inits the Apollo client with it.
|
|
358
359
|
*
|
|
359
|
-
* @param {*}
|
|
360
|
+
* @param {String} token The token *value* (not the `{ id, value }` object).
|
|
360
361
|
*/
|
|
361
362
|
setToken(token) {
|
|
362
363
|
this.token = token;
|
|
@@ -379,7 +380,11 @@ class IdentityX {
|
|
|
379
380
|
});
|
|
380
381
|
const { token } = data.impersonateAppUser;
|
|
381
382
|
tokenCookie.setTo(this.res, token.value);
|
|
382
|
-
|
|
383
|
+
// `setToken` expects the token string (as the constructor supplies from the
|
|
384
|
+
// cookie) — passing the `{ id, value }` object here produced an
|
|
385
|
+
// `AppUser [object Object]` authorization header on the re-inited client,
|
|
386
|
+
// and left `this.token` as an object for `logoutAppUser`.
|
|
387
|
+
this.setToken(token.value);
|
|
383
388
|
}
|
|
384
389
|
|
|
385
390
|
async logoutAppUser() {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default sink for `config.addRequestAttributes` — tags the current New Relic
|
|
3
|
+
* transaction, with no configuration and no New Relic dependency.
|
|
4
|
+
*
|
|
5
|
+
* The agent is used **only when the application has already loaded it**. Every
|
|
6
|
+
* marko-web site requires its `newrelic.js` at boot, so by the time a request
|
|
7
|
+
* is served the module is in `require.cache` and `require('newrelic')` returns
|
|
8
|
+
* that same initialized singleton. Requiring it unconditionally would instead
|
|
9
|
+
* *start* an agent on sites that don't use New Relic, which is why this checks
|
|
10
|
+
* the cache rather than just try/catching the require.
|
|
11
|
+
*
|
|
12
|
+
* Net effect: attributes appear fleet-wide on a dependency bump alone, and the
|
|
13
|
+
* whole thing is inert on any site without New Relic. A site can still pass its
|
|
14
|
+
* own `addRequestAttributes` to override.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
let resolvedPath;
|
|
18
|
+
let lookupFailed = false;
|
|
19
|
+
let agent = null;
|
|
20
|
+
|
|
21
|
+
const getAgent = () => {
|
|
22
|
+
if (agent) return agent;
|
|
23
|
+
if (lookupFailed) return null;
|
|
24
|
+
if (!resolvedPath) {
|
|
25
|
+
try {
|
|
26
|
+
resolvedPath = require.resolve('newrelic');
|
|
27
|
+
} catch (e) {
|
|
28
|
+
// Not installed — nothing to report to.
|
|
29
|
+
lookupFailed = true;
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// `newrelic` is intentionally NOT a dependency of this package: declaring it
|
|
34
|
+
// would install it on every monorail site, and requiring an uninitialized
|
|
35
|
+
// agent starts one. We only ever pick up an instance the host app already
|
|
36
|
+
// created, which is why the cache check above guards this require.
|
|
37
|
+
// eslint-disable-next-line global-require, import/no-extraneous-dependencies
|
|
38
|
+
if (require.cache[resolvedPath]) agent = require('newrelic');
|
|
39
|
+
return agent;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
module.exports = (attributes) => {
|
|
43
|
+
const nr = getAgent();
|
|
44
|
+
if (!nr || typeof nr.addCustomAttributes !== 'function') return;
|
|
45
|
+
try {
|
|
46
|
+
nr.addCustomAttributes(attributes);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
// Telemetry must never break a request.
|
|
49
|
+
}
|
|
50
|
+
};
|