Repository: microsoft/botbuilder-python

Issue:
[PORT] Move TelemetryClient property to DialogContainer
> Port this change from botbuilder-dotnet/master branch:
https://github.com/microsoft/botbuilder-dotnet/pull/4178

Fixes #2638 

Move TelemetryClient onto DialogContainer (implementation the same in AdaptiveDialog and ComponentDialog).


# Changed projects
* Microsoft.Bot.Builder.Dialogs.Adaptive
* Microsoft.Bot.Builder.Dialogs






Discussion:

Resolving patch:
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_container.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_container.py
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_container.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_container.py
@@ -4,6 +4,7 @@
 from abc import ABC, abstractmethod
 
 
+from botbuilder.core import NullTelemetryClient, BotTelemetryClient
 from .dialog import Dialog
 from .dialog_context import DialogContext
 from .dialog_event import DialogEvent
@@ -17,6 +18,31 @@ def __init__(self, dialog_id: str = None):
 
         self.dialogs = DialogSet()
 
+    @property
+    def telemetry_client(self) -> BotTelemetryClient:
+        """
+        Gets the telemetry client for logging events.
+        """
+        return self._telemetry_client
+
+    @telemetry_client.setter
+    def telemetry_client(self, value: BotTelemetryClient) -> None:
+        """
+        Sets the telemetry client for all dialogs in this set.
+        """
+        if value is None:
+            self._telemetry_client = NullTelemetryClient()
+        else:
+            self._telemetry_client = value
+
+        # Care! Dialogs.TelemetryClient assignment internally assigns the
+        # TelemetryClient for each dialog which could lead to an eventual stack
+        # overflow in cyclical dialog structures.
+        # Don't set the telemetry client if the candidate instance is the same as
+        # the currently set one.
+        if self.dialogs.telemetry_client != value:
+            self.dialogs.telemetry_client = self._telemetry_client
+
     @abstractmethod
     def create_child_context(self, dialog_context: DialogContext) -> DialogContext:
         raise NotImplementedError()
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_set.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_set.py
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_set.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_set.py
@@ -4,7 +4,13 @@
 from hashlib import sha256
 from typing import Dict
 
-from botbuilder.core import TurnContext, BotAssert, StatePropertyAccessor
+from botbuilder.core import (
+    NullTelemetryClient,
+    BotTelemetryClient,
+    TurnContext,
+    BotAssert,
+    StatePropertyAccessor,
+)
 from .dialog import Dialog
 from .dialog_state import DialogState
 
@@ -34,11 +40,31 @@ def __init__(self, dialog_state: StatePropertyAccessor = None):
                 del frame
 
         self._dialog_state = dialog_state
-        # self.__telemetry_client = NullBotTelemetryClient.Instance;
+        self.__telemetry_client = NullTelemetryClient()
 
         self._dialogs: Dict[str, Dialog] = {}
         self._version: str = None
 
+    @property
+    def telemetry_client(self) -> BotTelemetryClient:
+        """
+        Gets the telemetry client for logging events.
+        """
+        return self.__telemetry_client
+
+    @telemetry_client.setter
+    def telemetry_client(self, value: BotTelemetryClient) -> None:
+        """
+        Sets the telemetry client for all dialogs in this set.
+        """
+        if value is None:
+            self.__telemetry_client = NullTelemetryClient()
+        else:
+            self.__telemetry_client = value
+
+        for dialog in self._dialogs.values():
+            dialog.telemetry_client = self.__telemetry_client
+
     def get_version(self) -> str:
         """
         Gets a unique string which represents the combined versions of all dialogs in this this dialogset.


Test patch:
diff --git a/libraries/botbuilder-dialogs/tests/test_dialog_set.py b/libraries/botbuilder-dialogs/tests/test_dialog_set.py
--- a/libraries/botbuilder-dialogs/tests/test_dialog_set.py
+++ b/libraries/botbuilder-dialogs/tests/test_dialog_set.py
@@ -2,8 +2,15 @@
 # Licensed under the MIT License.
 
 import aiounittest
-from botbuilder.dialogs import DialogSet, ComponentDialog
-from botbuilder.core import ConversationState, MemoryStorage
+from botbuilder.dialogs import DialogSet, ComponentDialog, WaterfallDialog
+from botbuilder.core import ConversationState, MemoryStorage, NullTelemetryClient
+
+
+class MyBotTelemetryClient(NullTelemetryClient):
+    # pylint: disable=useless-return
+    def __init__(self):
+        super().__init__()
+        return
 
 
 class DialogSetTests(aiounittest.AsyncTestCase):
@@ -18,3 +25,85 @@ def test_dialogset_constructor_null_property(self):
 
     def test_dialogset_constructor_null_from_componentdialog(self):
         ComponentDialog("MyId")
+
+    def test_dialogset_telemetryset(self):
+        convo_state = ConversationState(MemoryStorage())
+        dialog_state_property = convo_state.create_property("dialogstate")
+        dialog_set = DialogSet(dialog_state_property)
+
+        dialog_set.add(WaterfallDialog("A"))
+        dialog_set.add(WaterfallDialog("B"))
+
+        self.assertTrue(
+            isinstance(
+                dialog_set.find_dialog("A").telemetry_client, NullTelemetryClient
+            )
+        )
+        self.assertTrue(
+            isinstance(
+                dialog_set.find_dialog("B").telemetry_client, NullTelemetryClient
+            )
+        )
+
+        dialog_set.telemetry_client = MyBotTelemetryClient()
+
+        self.assertTrue(
+            isinstance(
+                dialog_set.find_dialog("A").telemetry_client, MyBotTelemetryClient
+            )
+        )
+        self.assertTrue(
+            isinstance(
+                dialog_set.find_dialog("B").telemetry_client, MyBotTelemetryClient
+            )
+        )
+
+    def test_dialogset_nulltelemetryset(self):
+        convo_state = ConversationState(MemoryStorage())
+        dialog_state_property = convo_state.create_property("dialogstate")
+        dialog_set = DialogSet(dialog_state_property)
+
+        dialog_set.add(WaterfallDialog("A"))
+        dialog_set.add(WaterfallDialog("B"))
+
+        dialog_set.telemetry_client = MyBotTelemetryClient()
+        dialog_set.telemetry_client = None
+
+        self.assertFalse(
+            isinstance(
+                dialog_set.find_dialog("A").telemetry_client, MyBotTelemetryClient
+            )
+        )
+        self.assertFalse(
+            isinstance(
+                dialog_set.find_dialog("B").telemetry_client, MyBotTelemetryClient
+            )
+        )
+        self.assertTrue(
+            isinstance(
+                dialog_set.find_dialog("A").telemetry_client, NullTelemetryClient
+            )
+        )
+        self.assertTrue(
+            isinstance(
+                dialog_set.find_dialog("B").telemetry_client, NullTelemetryClient
+            )
+        )
+
+    # pylint: disable=pointless-string-statement
+    """
+    This test will be enabled when telematry tests are fixed for DialogSet telemetry
+    def test_dialogset_addtelemetryset(self):
+        convo_state = ConversationState(MemoryStorage())
+        dialog_state_property = convo_state.create_property("dialogstate")
+        dialog_set = DialogSet(dialog_state_property)
+
+        dialog_set.add(WaterfallDialog("A"))
+        dialog_set.add(WaterfallDialog("B"))
+
+        dialog_set.telemetry_client = MyBotTelemetryClient()
+
+        dialog_set.add(WaterfallDialog("C"))
+
+        self.assertTrue(isinstance(dialog_set.find_dialog("C").telemetry_client, MyBotTelemetryClient))
+    """