Przeglądaj źródła

Merge pull request #26994 from overleaf/rh-cio-ai-assist

Expose AI status to customer.io

GitOrigin-RevId: 7e69676d686326f50b16cbae9b1e4d97c14612fa
roo hutton 1 rok temu
rodzic
commit
fe8964cc0a

+ 35 - 12
services/web/app/src/Features/Project/ProjectListController.mjs

@@ -423,6 +423,8 @@ async function projectListPage(req, res, next) {
   // customer.io: Premium nudge experiment
   // Only do customer-io-trial-conversion assignment for users not in India/China and not in group/commons
   let customerIoEnabled = false
+  const aiBlocked = !(await _canUseAIAssist(user))
+  const hasAiAssist = await _userHasAIAssist(user)
   if (!userIsMemberOfGroupSubscription && !inEnterpriseCommons) {
     try {
       const ip = req.ip
@@ -487,6 +489,8 @@ async function projectListPage(req, res, next) {
     userRestrictions: Array.from(req.userRestrictions || []),
     customerIoEnabled,
     showAiAssistNotification,
+    aiBlocked,
+    hasAiAssist,
   })
 }
 
@@ -771,6 +775,35 @@ function _hasActiveFilter(filters) {
   )
 }
 
+async function _userHasAIAssist(user) {
+  // Check if the user has AI Assist enabled via Overleaf
+  if (user.features?.aiErrorAssistant) {
+    return true
+  }
+  // Check if the user has AI Assist enabled via Writefull
+  const { isPremium: hasAiAssistViaWritefull } =
+    await UserGetter.promises.getWritefullData(user._id)
+  if (hasAiAssistViaWritefull) {
+    return true
+  }
+  return false
+}
+
+// Determines if user is able to enable AI assist
+// based on their permissions and settings
+// It does NOT determine if the user has AI Assist enabled
+async function _canUseAIAssist(user) {
+  // Check if the assistant has been manually disabled by the user
+  if (user.aiErrorAssistant?.enabled === false) {
+    return false
+  }
+
+  // Check if the user can use AI features (policy check)
+  return await PermissionsManager.promises.checkUserPermissions(user, [
+    'use-ai',
+  ])
+}
+
 async function _showAiAssistNotification(user) {
   // Check if the assistant has been manually disabled by the user
   if (user.aiErrorAssistant?.enabled === false) {
@@ -794,18 +827,8 @@ async function _showAiAssistNotification(user) {
     return false
   }
 
-  // Check if the user already has AI Assist via Overleaf
-  if (user.features?.aiErrorAssistant) {
-    return false
-  }
-  // Check if the user already has AI Assist via Writefull
-  const { isPremium: hasAiAssistViaWritefull } =
-    await UserGetter.promises.getWritefullData(user._id)
-  if (hasAiAssistViaWritefull) {
-    return false
-  }
-
-  return true
+  const userHasAiAssist = await _userHasAIAssist(user)
+  return !userHasAiAssist
 }
 
 export default {

+ 24 - 3
services/web/app/views/_customer_io.pug

@@ -1,11 +1,22 @@
 if(customerIoEnabled && ExposedSettings.cioWriteKey && ExposedSettings.cioSiteId)
-	script(type="text/javascript", id="cio-loader", nonce=scriptNonce, data-best-subscription=(usersBestSubscription && usersBestSubscription.type), data-cio-write-key=ExposedSettings.cioWriteKey, data-cio-site-id=ExposedSettings.cioSiteId, data-session-analytics-id=getSessionAnalyticsId(), data-user-id=getLoggedInUserId()).
+	-
+		function boolAttr(value) {
+			return value !== undefined ? String(value) : null;
+		}
+	script(type="text/javascript", id="cio-loader", nonce=scriptNonce, data-best-subscription=(usersBestSubscription && usersBestSubscription.type), data-ai-blocked=boolAttr(aiBlocked), data-has-ai-assist=boolAttr(hasAiAssist), data-cio-write-key=ExposedSettings.cioWriteKey, data-cio-site-id=ExposedSettings.cioSiteId, data-session-analytics-id=getSessionAnalyticsId(), data-user-id=getLoggedInUserId()).
+		
+		function parseBool(value) {
+			return value === 'true' ? true : value === 'false' ? false : undefined;
+		}
+		
 		var cioSettings = document.querySelector('#cio-loader').dataset;
 		var analyticsId = cioSettings.sessionAnalyticsId;
 		var siteId = cioSettings.cioSiteId;
 		var writeKey = cioSettings.cioWriteKey;
 		var userId = cioSettings.userId;
-		var usersBestSubscription = cioSettings.bestSubscription
+		var usersBestSubscription = cioSettings.bestSubscription;
+		var aiBlocked = parseBool(cioSettings.aiBlocked);
+		var hasAiAssist = parseBool(cioSettings.hasAiAssist);
 
 
 		!function(){var i="cioanalytics", analytics=(window[i]=window[i]||[]);if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","reset","group","track","ready","alias","debug","page","once","off","on","addSourceMiddleware","addIntegrationMiddleware","setAnonymousId","addDestinationMiddleware"];analytics.factory=function(e){return function(){var t=Array.prototype.slice.call(arguments);t.unshift(e);analytics.push(t);return analytics}};for(var e=0;e<analytics.methods.length;e++){var key=analytics.methods[e];analytics[key]=analytics.factory(key)}analytics.load=function(key,e){var t=document.createElement("script");t.type="text/javascript";t.async=!0;t.setAttribute('data-global-customerio-analytics-key', i);t.src="https://cdp.customer.io/v1/analytics-js/snippet/" + key + "/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(t,n);analytics._writeKey=key;analytics._loadOptions=e};analytics.SNIPPET_VERSION="4.15.3";
@@ -23,5 +34,15 @@ if(customerIoEnabled && ExposedSettings.cioWriteKey && ExposedSettings.cioSiteId
 			if (analyticsId) {
 				analytics.setAnonymousId(analyticsId);
 			};
-			analytics.identify(analyticsId, {overleafId: userId, 'best-subscription-type': usersBestSubscription  });
+			
+			function addIfDefined(obj, key, value) {
+				if (typeof value !== 'undefined') obj[key] = value;
+			}
+			
+			var identifyData = {overleafId: userId};
+			addIfDefined(identifyData, 'best-subscription-type', usersBestSubscription);
+			addIfDefined(identifyData, 'aiBlocked', aiBlocked);
+			addIfDefined(identifyData, 'hasAiAssist', hasAiAssist);
+			
+			analytics.identify(analyticsId, identifyData);
 		}}();

+ 1 - 0
services/web/test/unit/src/Project/ProjectListController.test.mjs

@@ -87,6 +87,7 @@ describe('ProjectListController', function () {
       promises: {
         getUsers: sinon.stub().resolves(ctx.usersArr),
         getUserFullEmails: sinon.stub().resolves([]),
+        getWritefullData: sinon.stub().resolves({ isPremium: true }),
       },
     }
     ctx.Features = {