From 4af6f1415fe7892348e994737b01398d490e774a Mon Sep 17 00:00:00 2001 From: isshaddad Date: Fri, 24 Jul 2026 12:36:08 -0400 Subject: [PATCH 1/4] chore(webapp): migrate off deprecated @team-plain/typescript-sdk --- .../app/routes/api.v1.plain.customer-cards.ts | 2 +- apps/webapp/app/routes/resources.feedback.ts | 4 +- ...ces.orgs.$organizationSlug.select-plan.tsx | 2 +- apps/webapp/app/utils/plain.server.ts | 81 ++++++++++--------- .../v3/services/setBranchesAddOn.server.ts | 2 +- .../v3/services/setConcurrencyAddOn.server.ts | 2 +- .../v3/services/setSchedulesAddOn.server.ts | 2 +- .../app/v3/services/setSeatsAddOn.server.ts | 2 +- apps/webapp/package.json | 3 +- pnpm-lock.yaml | 40 +++++---- 10 files changed, 78 insertions(+), 62 deletions(-) diff --git a/apps/webapp/app/routes/api.v1.plain.customer-cards.ts b/apps/webapp/app/routes/api.v1.plain.customer-cards.ts index ad869c7a4f7..5e09fb08544 100644 --- a/apps/webapp/app/routes/api.v1.plain.customer-cards.ts +++ b/apps/webapp/app/routes/api.v1.plain.customer-cards.ts @@ -1,6 +1,6 @@ import { json, type ActionFunctionArgs } from "@remix-run/server-runtime"; import { timingSafeEqual } from "crypto"; -import { uiComponent } from "@team-plain/typescript-sdk"; +import { uiComponent } from "@team-plain/ui-components"; import { z } from "zod"; import { prisma } from "~/db.server"; import { env } from "~/env.server"; diff --git a/apps/webapp/app/routes/resources.feedback.ts b/apps/webapp/app/routes/resources.feedback.ts index bdb16ab7991..4ace262f0cb 100644 --- a/apps/webapp/app/routes/resources.feedback.ts +++ b/apps/webapp/app/routes/resources.feedback.ts @@ -1,13 +1,11 @@ import { parseWithZod } from "@conform-to/zod"; import { type ActionFunctionArgs, json } from "@remix-run/server-runtime"; -import { type PlainClient, uiComponent } from "@team-plain/typescript-sdk"; +import { uiComponent } from "@team-plain/ui-components"; import { z } from "zod"; import { redirectWithSuccessMessage } from "~/models/message.server"; import { requireUser } from "~/services/session.server"; import { sendToPlain } from "~/utils/plain.server"; -let _client: PlainClient | undefined; - export const feedbackTypes = { bug: { label: "Bug report", diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx index 2c107c35b13..aec92240d31 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx @@ -1,7 +1,7 @@ import { CheckIcon, XMarkIcon } from "@heroicons/react/20/solid"; import { ArrowDownCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/24/outline"; import { Form, useLocation, useNavigation } from "@remix-run/react"; -import { uiComponent } from "@team-plain/typescript-sdk"; +import { uiComponent } from "@team-plain/ui-components"; import { type AddOnPricing, type FreePlanDefinition, diff --git a/apps/webapp/app/utils/plain.server.ts b/apps/webapp/app/utils/plain.server.ts index c17b2f3f006..1dc4e23bf9d 100644 --- a/apps/webapp/app/utils/plain.server.ts +++ b/apps/webapp/app/utils/plain.server.ts @@ -1,5 +1,5 @@ -import type { uiComponent } from "@team-plain/typescript-sdk"; -import { PlainClient } from "@team-plain/typescript-sdk"; +import { PlainClient } from "@team-plain/graphql"; +import type { uiComponent } from "@team-plain/ui-components"; import { env } from "~/env.server"; type Input = { @@ -20,43 +20,50 @@ export async function sendToPlain({ userId, email, name, title, components, labe apiKey: env.PLAIN_API_KEY, }); - const upsertCustomerRes = await client.upsertCustomer({ - identifier: { - emailAddress: email, - }, - onCreate: { - externalId: userId, - fullName: name, - email: { - email: email, - isVerified: true, + // Best-effort support side-effect: the new client throws on failure, so we swallow and log + // rather than break the user action that triggered it. + try { + const upsertCustomerRes = await client.mutation.upsertCustomer({ + input: { + identifier: { + emailAddress: email, + }, + onCreate: { + externalId: userId, + fullName: name, + email: { + email: email, + isVerified: true, + }, + }, + onUpdate: { + externalId: { value: userId }, + fullName: { value: name }, + email: { + email: email, + isVerified: true, + }, + }, }, - }, - onUpdate: { - externalId: { value: userId }, - fullName: { value: name }, - email: { - email: email, - isVerified: true, - }, - }, - }); + }); - if (upsertCustomerRes.error) { - console.error("Failed to upsert customer in Plain", upsertCustomerRes.error); - return; - } + const customerId = upsertCustomerRes.customer?.id; + if (!customerId) { + console.error("Failed to upsert customer in Plain", upsertCustomerRes.result); + return; + } - const createThreadRes = await client.createThread({ - customerIdentifier: { - customerId: upsertCustomerRes.data.customer.id, - }, - title: title, - components: components, - labelTypeIds, - }); - - if (createThreadRes.error) { - console.error("Failed to create thread in Plain", createThreadRes.error); + await client.mutation.createThread({ + input: { + customerIdentifier: { + customerId, + }, + title: title, + components: components, + labelTypeIds, + }, + }); + } catch (error) { + console.error("Failed to send to Plain", error); } } diff --git a/apps/webapp/app/v3/services/setBranchesAddOn.server.ts b/apps/webapp/app/v3/services/setBranchesAddOn.server.ts index 55f4f5817d8..26d153d99f3 100644 --- a/apps/webapp/app/v3/services/setBranchesAddOn.server.ts +++ b/apps/webapp/app/v3/services/setBranchesAddOn.server.ts @@ -3,7 +3,7 @@ import { tryCatch } from "@trigger.dev/core/utils"; import { setBranchesAddOn } from "~/services/platform.v3.server"; import assertNever from "assert-never"; import { sendToPlain } from "~/utils/plain.server"; -import { uiComponent } from "@team-plain/typescript-sdk"; +import { uiComponent } from "@team-plain/ui-components"; type Input = { userId: string; diff --git a/apps/webapp/app/v3/services/setConcurrencyAddOn.server.ts b/apps/webapp/app/v3/services/setConcurrencyAddOn.server.ts index 6daf970bbd6..7912c118994 100644 --- a/apps/webapp/app/v3/services/setConcurrencyAddOn.server.ts +++ b/apps/webapp/app/v3/services/setConcurrencyAddOn.server.ts @@ -4,7 +4,7 @@ import { tryCatch } from "@trigger.dev/core"; import { setConcurrencyAddOn } from "~/services/platform.v3.server"; import assertNever from "assert-never"; import { sendToPlain } from "~/utils/plain.server"; -import { uiComponent } from "@team-plain/typescript-sdk"; +import { uiComponent } from "@team-plain/ui-components"; type Input = { userId: string; diff --git a/apps/webapp/app/v3/services/setSchedulesAddOn.server.ts b/apps/webapp/app/v3/services/setSchedulesAddOn.server.ts index a96a6000765..5a3e64f6c63 100644 --- a/apps/webapp/app/v3/services/setSchedulesAddOn.server.ts +++ b/apps/webapp/app/v3/services/setSchedulesAddOn.server.ts @@ -3,7 +3,7 @@ import { tryCatch } from "@trigger.dev/core/utils"; import { setSchedulesAddOn } from "~/services/platform.v3.server"; import assertNever from "assert-never"; import { sendToPlain } from "~/utils/plain.server"; -import { uiComponent } from "@team-plain/typescript-sdk"; +import { uiComponent } from "@team-plain/ui-components"; type Input = { userId: string; diff --git a/apps/webapp/app/v3/services/setSeatsAddOn.server.ts b/apps/webapp/app/v3/services/setSeatsAddOn.server.ts index b9159c2ee7c..d17c61c0da0 100644 --- a/apps/webapp/app/v3/services/setSeatsAddOn.server.ts +++ b/apps/webapp/app/v3/services/setSeatsAddOn.server.ts @@ -3,7 +3,7 @@ import { tryCatch } from "@trigger.dev/core/utils"; import { setSeatsAddOn } from "~/services/platform.v3.server"; import assertNever from "assert-never"; import { sendToPlain } from "~/utils/plain.server"; -import { uiComponent } from "@team-plain/typescript-sdk"; +import { uiComponent } from "@team-plain/ui-components"; type Input = { userId: string; diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 678eeb76033..c51882d0037 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -112,7 +112,8 @@ "@tanstack/match-sorter-utils": "^8.19.4", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.0.4", - "@team-plain/typescript-sdk": "^3.5.0", + "@team-plain/graphql": "^1.3.0", + "@team-plain/ui-components": "^5.0.0", "@trigger.dev/companyicons": "^1.5.35", "@trigger.dev/core": "workspace:*", "@trigger.dev/database": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b09dc718a6f..787406e25e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -462,9 +462,12 @@ importers: '@tanstack/react-virtual': specifier: ^3.0.4 version: 3.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@team-plain/typescript-sdk': - specifier: ^3.5.0 - version: 3.5.0 + '@team-plain/graphql': + specifier: ^1.3.0 + version: 1.3.0 + '@team-plain/ui-components': + specifier: ^5.0.0 + version: 5.0.0(@team-plain/graphql@1.3.0) '@trigger.dev/companyicons': specifier: ^1.5.35 version: 1.5.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -7659,9 +7662,13 @@ packages: '@tanstack/virtual-core@3.0.0': resolution: {integrity: sha512-SYXOBTjJb05rXa2vl55TTwO40A6wKu0R5i1qQwhJYNDIqaIGF7D0HsLw+pJAyi2OvntlEIVusx3xtbbgSUi6zg==} - '@team-plain/typescript-sdk@3.5.0': - resolution: {integrity: sha512-9kweiSlYAN31VI7yzILGxdlZqsGJ+FmCEfXyEZ/0/i3r6vOwq45FDqtjadnQJVtFm+rf/8vCFRN+wEYMIEv6Aw==} - deprecated: This package is now deprecated. Please use @team-plain/graphql, @team-plain/webhooks and @team-plain/ui-components (https://github.com/team-plain/sdk) + '@team-plain/graphql@1.3.0': + resolution: {integrity: sha512-e+AZ9sIn4uliAyg7efcX6F0L4qOuwqL+6a+f5qe7Bfp7yXB5DU3vwECxHWN9YCD30MXnJ3oeS8Co1T5C5r0DUQ==} + + '@team-plain/ui-components@5.0.0': + resolution: {integrity: sha512-7/k8GggIGhdzkrZD3LAYWQZgJZi7VB3bQkpGbL67VrfoQGLYRumCMJ8jEvfLgX8n/tJ6Xn03u5etwLBBeb+Kqw==} + peerDependencies: + '@team-plain/graphql': 1.3.0 '@testcontainers/postgresql@11.14.0': resolution: {integrity: sha512-wYbJn8GRTj8qfqzfVubxioYWlHJU/ImIjuzPwyy9C5Qfo6g3GLduPZAj+BifvqTZjgT3gd4gFVLCPhBji7dc1w==} @@ -10742,8 +10749,8 @@ packages: grapheme-splitter@1.0.4: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} - graphql@16.6.0: - resolution: {integrity: sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==} + graphql@16.14.2: + resolution: {integrity: sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} gunzip-maybe@1.4.2: @@ -18569,9 +18576,9 @@ snapshots: '@google-cloud/precise-date@4.0.0': {} - '@graphql-typed-document-node/core@3.2.0(graphql@16.6.0)': + '@graphql-typed-document-node/core@3.2.0(graphql@16.14.2)': dependencies: - graphql: 16.6.0 + graphql: 16.14.2 '@grpc/grpc-js@1.12.6': dependencies: @@ -22303,11 +22310,14 @@ snapshots: '@tanstack/virtual-core@3.0.0': {} - '@team-plain/typescript-sdk@3.5.0': + '@team-plain/graphql@1.3.0': dependencies: - '@graphql-typed-document-node/core': 3.2.0(graphql@16.6.0) - graphql: 16.6.0 - zod: 3.25.76 + '@graphql-typed-document-node/core': 3.2.0(graphql@16.14.2) + graphql: 16.14.2 + + '@team-plain/ui-components@5.0.0(@team-plain/graphql@1.3.0)': + dependencies: + '@team-plain/graphql': 1.3.0 '@testcontainers/postgresql@11.14.0': dependencies: @@ -25974,7 +25984,7 @@ snapshots: grapheme-splitter@1.0.4: {} - graphql@16.6.0: {} + graphql@16.14.2: {} gunzip-maybe@1.4.2: dependencies: From c598f5a86135070fbe4f50d61e7e1074115eddd5 Mon Sep 17 00:00:00 2001 From: isshaddad Date: Fri, 24 Jul 2026 13:54:49 -0400 Subject: [PATCH 2/4] feat(webapp): attribute Plain support threads to their org tenant --- ...ces.orgs.$organizationSlug.select-plan.tsx | 2 + apps/webapp/app/utils/plain.server.ts | 42 ++++++++++++++++++- .../v3/services/setBranchesAddOn.server.ts | 2 + .../v3/services/setConcurrencyAddOn.server.ts | 2 + .../v3/services/setSchedulesAddOn.server.ts | 2 + .../app/v3/services/setSeatsAddOn.server.ts | 2 + 6 files changed, 51 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx index aec92240d31..b72033098b7 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx @@ -95,6 +95,8 @@ export const action = dashboardAction( email: user.email, name: user.name ?? "", title: "Plan cancelation feedback", + organizationId: organization.id, + organizationName: organization.title, components: [ uiComponent.text({ text: `${user.name} (${user.email}) just canceled their plan.`, diff --git a/apps/webapp/app/utils/plain.server.ts b/apps/webapp/app/utils/plain.server.ts index 1dc4e23bf9d..a702e81a626 100644 --- a/apps/webapp/app/utils/plain.server.ts +++ b/apps/webapp/app/utils/plain.server.ts @@ -9,9 +9,20 @@ type Input = { title: string; components: ReturnType[]; labelTypeIds?: string[]; + organizationId?: string; + organizationName?: string; }; -export async function sendToPlain({ userId, email, name, title, components, labelTypeIds }: Input) { +export async function sendToPlain({ + userId, + email, + name, + title, + components, + labelTypeIds, + organizationId, + organizationName, +}: Input) { if (!env.PLAIN_API_KEY) { return; } @@ -53,6 +64,34 @@ export async function sendToPlain({ userId, email, name, title, components, labe return; } + // Attribute the thread to the org so support data can be rolled up per org: the tenant is + // keyed by externalId = org_id. Isolated in its own try/catch, and the thread's + // tenantIdentifier is gated on success — so a tenant failure (e.g. an API key without + // tenant scope) downgrades to "no attribution" instead of dropping the thread. The + // customer's own externalId (User.id, used by the customer cards + impersonation link) is + // left untouched. + let tenantLinked = false; + if (organizationId) { + try { + await client.mutation.upsertTenant({ + input: { + identifier: { externalId: organizationId }, + externalId: organizationId, + name: organizationName ?? organizationId, + }, + }); + await client.mutation.addCustomerToTenants({ + input: { + customerIdentifier: { customerId }, + tenantIdentifiers: [{ externalId: organizationId }], + }, + }); + tenantLinked = true; + } catch (error) { + console.error("Failed to link Plain customer to org tenant", error); + } + } + await client.mutation.createThread({ input: { customerIdentifier: { @@ -61,6 +100,7 @@ export async function sendToPlain({ userId, email, name, title, components, labe title: title, components: components, labelTypeIds, + tenantIdentifier: tenantLinked ? { externalId: organizationId } : undefined, }, }); } catch (error) { diff --git a/apps/webapp/app/v3/services/setBranchesAddOn.server.ts b/apps/webapp/app/v3/services/setBranchesAddOn.server.ts index 26d153d99f3..b14aebfbe9d 100644 --- a/apps/webapp/app/v3/services/setBranchesAddOn.server.ts +++ b/apps/webapp/app/v3/services/setBranchesAddOn.server.ts @@ -74,6 +74,8 @@ export class SetBranchesAddOnService extends BaseService { email: user.email, name: user.name ?? user.displayName ?? user.email, title: `Preview branches quota request: ${amount}`, + organizationId, + organizationName: organization?.title, components: [ uiComponent.text({ text: `Org: ${organization?.title} (${organizationId})`, diff --git a/apps/webapp/app/v3/services/setConcurrencyAddOn.server.ts b/apps/webapp/app/v3/services/setConcurrencyAddOn.server.ts index 7912c118994..da1f61b37fc 100644 --- a/apps/webapp/app/v3/services/setConcurrencyAddOn.server.ts +++ b/apps/webapp/app/v3/services/setConcurrencyAddOn.server.ts @@ -106,6 +106,8 @@ export class SetConcurrencyAddOnService extends BaseService { email: user.email, name: user.name ?? user.displayName ?? user.email, title: `Concurrency quota request: ${totalExtraConcurrency}`, + organizationId, + organizationName: organization?.title, components: [ uiComponent.text({ text: `Org: ${organization?.title} (${organizationId})`, diff --git a/apps/webapp/app/v3/services/setSchedulesAddOn.server.ts b/apps/webapp/app/v3/services/setSchedulesAddOn.server.ts index 5a3e64f6c63..42b3d120363 100644 --- a/apps/webapp/app/v3/services/setSchedulesAddOn.server.ts +++ b/apps/webapp/app/v3/services/setSchedulesAddOn.server.ts @@ -74,6 +74,8 @@ export class SetSchedulesAddOnService extends BaseService { email: user.email, name: user.name ?? user.displayName ?? user.email, title: `Schedules quota request: ${amount}`, + organizationId, + organizationName: organization?.title, components: [ uiComponent.text({ text: `Org: ${organization?.title} (${organizationId})`, diff --git a/apps/webapp/app/v3/services/setSeatsAddOn.server.ts b/apps/webapp/app/v3/services/setSeatsAddOn.server.ts index d17c61c0da0..a761e7f9762 100644 --- a/apps/webapp/app/v3/services/setSeatsAddOn.server.ts +++ b/apps/webapp/app/v3/services/setSeatsAddOn.server.ts @@ -74,6 +74,8 @@ export class SetSeatsAddOnService extends BaseService { email: user.email, name: user.name ?? user.displayName ?? user.email, title: `Seats quota request: ${amount}`, + organizationId, + organizationName: organization?.title, components: [ uiComponent.text({ text: `Org: ${organization?.title} (${organizationId})`, From f5f4fa38bb9216ad5382d388e2252b9d4b3f29d8 Mon Sep 17 00:00:00 2001 From: isshaddad Date: Fri, 24 Jul 2026 14:29:00 -0400 Subject: [PATCH 3/4] fix(webapp): check Plain mutation result.error, not just thrown errors --- apps/webapp/app/utils/plain.server.ts | 42 ++++++++++++++++++--------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/apps/webapp/app/utils/plain.server.ts b/apps/webapp/app/utils/plain.server.ts index a702e81a626..2dde84128e3 100644 --- a/apps/webapp/app/utils/plain.server.ts +++ b/apps/webapp/app/utils/plain.server.ts @@ -31,8 +31,8 @@ export async function sendToPlain({ apiKey: env.PLAIN_API_KEY, }); - // Best-effort support side-effect: the new client throws on failure, so we swallow and log - // rather than break the user action that triggered it. + // Best-effort support side-effect. Only transport/auth errors throw (caught below); business + // and validation failures come back in each mutation's `result.error`, so we check those inline. try { const upsertCustomerRes = await client.mutation.upsertCustomer({ input: { @@ -58,11 +58,11 @@ export async function sendToPlain({ }, }); - const customerId = upsertCustomerRes.customer?.id; - if (!customerId) { - console.error("Failed to upsert customer in Plain", upsertCustomerRes.result); + if (upsertCustomerRes.error || !upsertCustomerRes.customer?.id) { + console.error("Failed to upsert customer in Plain", upsertCustomerRes.error); return; } + const customerId = upsertCustomerRes.customer.id; // Attribute the thread to the org so support data can be rolled up per org: the tenant is // keyed by externalId = org_id. Isolated in its own try/catch, and the thread's @@ -73,26 +73,37 @@ export async function sendToPlain({ let tenantLinked = false; if (organizationId) { try { - await client.mutation.upsertTenant({ + const tenantRes = await client.mutation.upsertTenant({ input: { identifier: { externalId: organizationId }, externalId: organizationId, name: organizationName ?? organizationId, }, }); - await client.mutation.addCustomerToTenants({ - input: { - customerIdentifier: { customerId }, - tenantIdentifiers: [{ externalId: organizationId }], - }, - }); - tenantLinked = true; + // Only link + attribute if the tenant genuinely upserted — a mutation error comes back in + // `.error` (not thrown), and stamping the thread with a tenant that wasn't created would + // make createThread itself fail. + const membershipRes = tenantRes.error + ? undefined + : await client.mutation.addCustomerToTenants({ + input: { + customerIdentifier: { customerId }, + tenantIdentifiers: [{ externalId: organizationId }], + }, + }); + if (tenantRes.error) { + console.error("Failed to upsert Plain tenant", tenantRes.error); + } else if (membershipRes?.error) { + console.error("Failed to link Plain customer to tenant", membershipRes.error); + } else { + tenantLinked = true; + } } catch (error) { console.error("Failed to link Plain customer to org tenant", error); } } - await client.mutation.createThread({ + const threadRes = await client.mutation.createThread({ input: { customerIdentifier: { customerId, @@ -103,6 +114,9 @@ export async function sendToPlain({ tenantIdentifier: tenantLinked ? { externalId: organizationId } : undefined, }, }); + if (threadRes.error) { + console.error("Failed to create Plain thread", threadRes.error); + } } catch (error) { console.error("Failed to send to Plain", error); } From 4135bbb791404d2872520808c5789528212a0447 Mon Sep 17 00:00:00 2001 From: isshaddad Date: Fri, 24 Jul 2026 19:06:43 -0400 Subject: [PATCH 4/4] chore(webapp): add server-changes release note for the Plain migration --- .server-changes/plain-graphql-migration.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .server-changes/plain-graphql-migration.md diff --git a/.server-changes/plain-graphql-migration.md b/.server-changes/plain-graphql-migration.md new file mode 100644 index 00000000000..81468a91444 --- /dev/null +++ b/.server-changes/plain-graphql-migration.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: improvement +--- + +In-app feedback and add-on/quota requests are now recorded with more account context for the support team.